@semiont/react-ui 0.4.20 → 0.4.22

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 (125) hide show
  1. package/README.md +8 -5
  2. package/dist/{PdfAnnotationCanvas.client-CHDCGQBR.mjs → PdfAnnotationCanvas.client-5QESNO5H.mjs} +13 -16
  3. package/dist/PdfAnnotationCanvas.client-5QESNO5H.mjs.map +1 -0
  4. package/dist/TranslationManager-9Xj3MIWQ.d.mts +16 -0
  5. package/dist/chunk-4NOUO3W6.mjs +7788 -0
  6. package/dist/chunk-4NOUO3W6.mjs.map +1 -0
  7. package/dist/index.d.mts +212 -1206
  8. package/dist/index.mjs +3332 -13712
  9. package/dist/index.mjs.map +1 -1
  10. package/dist/test-utils.d.mts +48 -21
  11. package/dist/test-utils.mjs +2505 -87
  12. package/dist/test-utils.mjs.map +1 -1
  13. package/package.json +2 -2
  14. package/src/components/AnnotateReferencesProgressWidget.tsx +21 -28
  15. package/src/components/CodeMirrorRenderer.tsx +12 -12
  16. package/src/components/LiveRegion.tsx +1 -2
  17. package/src/components/StatusDisplay.tsx +42 -16
  18. package/src/components/Toolbar.tsx +4 -4
  19. package/src/components/__tests__/AnnotateReferencesProgressWidget.test.tsx +34 -20
  20. package/src/components/__tests__/StatusDisplay.test.tsx +50 -65
  21. package/src/components/__tests__/Toolbar.test.tsx +4 -4
  22. package/src/components/annotation/AnnotateToolbar.tsx +8 -9
  23. package/src/components/annotation/__tests__/AnnotateToolbar.test.tsx +31 -77
  24. package/src/components/annotation-popups/JsonLdView.tsx +1 -2
  25. package/src/components/annotation-popups/__tests__/JsonLdView.test.tsx +1 -2
  26. package/src/components/image-annotation/AnnotationOverlay.tsx +15 -18
  27. package/src/components/image-annotation/SvgDrawingCanvas.tsx +12 -17
  28. package/src/components/modals/ConfigureGenerationStep.tsx +1 -2
  29. package/src/components/modals/PermissionDeniedModal.tsx +11 -11
  30. package/src/components/modals/ReferenceWizardModal.tsx +14 -18
  31. package/src/components/modals/ResourceSearchModal.tsx +12 -8
  32. package/src/components/modals/SearchModal.tsx +11 -6
  33. package/src/components/modals/SearchResultsStep.tsx +1 -3
  34. package/src/components/modals/SessionExpiredModal.tsx +11 -11
  35. package/src/components/modals/__tests__/PermissionDeniedModal.test.tsx +7 -7
  36. package/src/components/modals/__tests__/ResourceSearchModal.test.tsx +10 -8
  37. package/src/components/modals/__tests__/SearchModal.accessibility.test.tsx +6 -2
  38. package/src/components/modals/__tests__/SearchModal.basic.test.tsx +6 -2
  39. package/src/components/modals/__tests__/SearchModal.keyboard.test.tsx +6 -2
  40. package/src/components/modals/__tests__/SearchModal.search-wiring.test.tsx +10 -7
  41. package/src/components/modals/__tests__/SearchModal.visual.test.tsx +6 -2
  42. package/src/components/modals/__tests__/SessionExpiredModal.test.tsx +5 -5
  43. package/src/components/navigation/CollapsibleResourceNavigation.tsx +10 -10
  44. package/src/components/navigation/ObservableLink.tsx +6 -6
  45. package/src/components/navigation/SimpleNavigation.tsx +4 -4
  46. package/src/components/navigation/__tests__/ObservableLink.test.tsx +4 -4
  47. package/src/components/navigation/__tests__/SimpleNavigation.test.tsx +4 -4
  48. package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +15 -18
  49. package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +1 -2
  50. package/src/components/resource/AnnotateView.tsx +8 -10
  51. package/src/components/resource/AnnotationHistory.tsx +9 -12
  52. package/src/components/resource/BrowseView.tsx +11 -8
  53. package/src/components/resource/ResourceViewer.tsx +22 -34
  54. package/src/components/resource/__tests__/AnnotationHistory.test.tsx +54 -192
  55. package/src/components/resource/__tests__/BrowseView.test.tsx +38 -87
  56. package/src/components/resource/__tests__/ResourceViewer.mode-switch.test.tsx +41 -31
  57. package/src/components/resource/__tests__/event-formatting.test.ts +6 -2
  58. package/src/components/resource/event-formatting.ts +2 -3
  59. package/src/components/resource/panels/AssessmentEntry.tsx +7 -8
  60. package/src/components/resource/panels/AssessmentPanel.tsx +21 -17
  61. package/src/components/resource/panels/AssistSection.tsx +15 -21
  62. package/src/components/resource/panels/CollaborationPanel.tsx +29 -7
  63. package/src/components/resource/panels/CommentEntry.tsx +7 -8
  64. package/src/components/resource/panels/CommentsPanel.tsx +11 -13
  65. package/src/components/resource/panels/HighlightEntry.tsx +7 -8
  66. package/src/components/resource/panels/HighlightPanel.tsx +12 -13
  67. package/src/components/resource/panels/ReferenceEntry.tsx +13 -15
  68. package/src/components/resource/panels/ReferencesPanel.tsx +17 -19
  69. package/src/components/resource/panels/ResourceInfoPanel.tsx +8 -7
  70. package/src/components/resource/panels/StatisticsPanel.tsx +2 -3
  71. package/src/components/resource/panels/TagEntry.tsx +7 -8
  72. package/src/components/resource/panels/TaggingPanel.tsx +14 -23
  73. package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +4 -3
  74. package/src/components/resource/panels/__tests__/AssessmentEntry.test.tsx +4 -4
  75. package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +22 -57
  76. package/src/components/resource/panels/__tests__/CollaborationPanel.test.tsx +51 -20
  77. package/src/components/resource/panels/__tests__/CommentEntry.test.tsx +4 -4
  78. package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +22 -61
  79. package/src/components/resource/panels/__tests__/HighlightEntry.test.tsx +4 -4
  80. package/src/components/resource/panels/__tests__/HighlightPanel.annotationProgress.test.tsx +1 -2
  81. package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +7 -8
  82. package/src/components/resource/panels/__tests__/ReferencesPanel.observable-flow.test.tsx +153 -0
  83. package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +51 -106
  84. package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +28 -53
  85. package/src/components/resource/panels/__tests__/StatisticsPanel.test.tsx +3 -3
  86. package/src/components/resource/panels/__tests__/TagEntry.test.tsx +4 -4
  87. package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +19 -52
  88. package/src/components/settings/SettingsPanel.tsx +9 -9
  89. package/src/components/settings/__tests__/SettingsPanel.test.tsx +15 -15
  90. package/src/features/admin-devops/components/AdminDevOpsPage.tsx +1 -2
  91. package/src/features/admin-exchange/components/AdminExchangePage.tsx +1 -1
  92. package/src/features/admin-exchange/components/ImportCard.tsx +2 -7
  93. package/src/features/admin-security/components/AdminSecurityPage.tsx +1 -2
  94. package/src/features/admin-users/components/AdminUsersPage.tsx +1 -1
  95. package/src/features/moderate-entity-tags/components/EntityTagsPage.tsx +1 -2
  96. package/src/features/moderate-recent/components/RecentDocumentsPage.tsx +1 -2
  97. package/src/features/moderate-tag-schemas/components/TagSchemasPage.tsx +1 -1
  98. package/src/features/moderation-linked-data/components/LinkedDataPage.tsx +1 -1
  99. package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +5 -3
  100. package/src/features/resource-compose/components/ResourceComposePage.tsx +6 -22
  101. package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +4 -3
  102. package/src/features/resource-discovery/components/ResourceCard.tsx +1 -2
  103. package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +3 -4
  104. package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +37 -45
  105. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +129 -197
  106. package/dist/KnowledgeBaseSessionContext-BNNunwzO.d.mts +0 -175
  107. package/dist/PdfAnnotationCanvas.client-CHDCGQBR.mjs.map +0 -1
  108. package/dist/chunk-OZICDVH7.mjs +0 -62
  109. package/dist/chunk-OZICDVH7.mjs.map +0 -1
  110. package/dist/chunk-R4CCMFJH.mjs +0 -877
  111. package/dist/chunk-R4CCMFJH.mjs.map +0 -1
  112. package/dist/chunk-VN5NY4SN.mjs +0 -200
  113. package/dist/chunk-VN5NY4SN.mjs.map +0 -1
  114. package/src/components/modals/ProposeEntitiesModal.tsx +0 -179
  115. package/src/components/modals/__tests__/ProposeEntitiesModal.test.tsx +0 -129
  116. package/src/features/resource-viewer/__tests__/AnnotationCreationPending.test.tsx +0 -323
  117. package/src/features/resource-viewer/__tests__/AnnotationDeletionIntegration.test.tsx +0 -245
  118. package/src/features/resource-viewer/__tests__/AnnotationProgressDismissal.test.tsx +0 -303
  119. package/src/features/resource-viewer/__tests__/BindFlowIntegration.test.tsx +0 -150
  120. package/src/features/resource-viewer/__tests__/DetectionFlowBug.test.tsx +0 -243
  121. package/src/features/resource-viewer/__tests__/DetectionFlowIntegration.test.tsx +0 -383
  122. package/src/features/resource-viewer/__tests__/ResourceMutations.test.tsx +0 -299
  123. package/src/features/resource-viewer/__tests__/ToastNotifications.test.tsx +0 -186
  124. package/src/features/resource-viewer/__tests__/YieldFlowIntegration.test.tsx +0 -429
  125. package/src/features/resource-viewer/__tests__/annotation-progress-flow.test.tsx +0 -348
@@ -36,7 +36,10 @@ vi.mock('../../../../contexts/TranslationContext', () => ({
36
36
 
37
37
  describe('CollaborationPanel Component', () => {
38
38
  const defaultProps = {
39
- isConnected: false,
39
+ // `degraded` is the "sustained disconnect" state — matches the
40
+ // pre-state-machine `isConnected: false` default, which was always
41
+ // used in tests to mean "the UI should show Disconnected."
42
+ state: 'degraded' as const,
40
43
  eventCount: 0,
41
44
  };
42
45
 
@@ -72,19 +75,19 @@ describe('CollaborationPanel Component', () => {
72
75
 
73
76
  describe('Connection Status', () => {
74
77
  it('should show disconnected status when not connected', () => {
75
- render(<CollaborationPanel {...defaultProps} isConnected={false} />);
78
+ render(<CollaborationPanel {...defaultProps} state="degraded" />);
76
79
 
77
80
  expect(screen.getByText('Disconnected')).toBeInTheDocument();
78
81
  });
79
82
 
80
83
  it('should show live status when connected', () => {
81
- render(<CollaborationPanel {...defaultProps} isConnected={true} />);
84
+ render(<CollaborationPanel {...defaultProps} state="open" />);
82
85
 
83
86
  expect(screen.getByText('Live')).toBeInTheDocument();
84
87
  });
85
88
 
86
89
  it('should show indicator when disconnected', () => {
87
- const { container } = render(<CollaborationPanel {...defaultProps} isConnected={false} />);
90
+ const { container } = render(<CollaborationPanel {...defaultProps} state="degraded" />);
88
91
 
89
92
  const indicator = container.querySelector('.semiont-collaboration-panel__dot');
90
93
  expect(indicator).toBeInTheDocument();
@@ -92,7 +95,7 @@ describe('CollaborationPanel Component', () => {
92
95
  });
93
96
 
94
97
  it('should show indicator when connected', () => {
95
- const { container } = render(<CollaborationPanel {...defaultProps} isConnected={true} />);
98
+ const { container } = render(<CollaborationPanel {...defaultProps} state="open" />);
96
99
 
97
100
  const indicator = container.querySelector('.semiont-collaboration-panel__dot');
98
101
  expect(indicator).toBeInTheDocument();
@@ -100,7 +103,7 @@ describe('CollaborationPanel Component', () => {
100
103
  });
101
104
 
102
105
  it('should use appropriate status text for disconnected state', () => {
103
- render(<CollaborationPanel {...defaultProps} isConnected={false} />);
106
+ render(<CollaborationPanel {...defaultProps} state="degraded" />);
104
107
 
105
108
  const statusText = screen.getByText('Disconnected');
106
109
  expect(statusText).toHaveClass('semiont-collaboration-panel__status-text');
@@ -108,35 +111,63 @@ describe('CollaborationPanel Component', () => {
108
111
  });
109
112
 
110
113
  it('should use appropriate status text for connected state', () => {
111
- render(<CollaborationPanel {...defaultProps} isConnected={true} />);
114
+ render(<CollaborationPanel {...defaultProps} state="open" />);
112
115
 
113
116
  const statusText = screen.getByText('Live');
114
117
  expect(statusText).toHaveClass('semiont-collaboration-panel__status-text');
115
118
  expect(statusText).toHaveAttribute('data-connected', 'true');
116
119
  });
120
+
121
+ // ── State-machine aware cases (post-CONNECTION-STATE) ─────────────
122
+ // These exercise the core reason CONNECTION-STATE exists: brief
123
+ // reconnect/connect cycles must NOT flash "Disconnected", or
124
+ // Strict-Mode mount churn makes the UI lie.
125
+
126
+ it('shows Live during brief `reconnecting` (does not alarm on churn)', () => {
127
+ render(<CollaborationPanel {...defaultProps} state="reconnecting" />);
128
+ expect(screen.getByText('Live')).toBeInTheDocument();
129
+ expect(screen.queryByText('Disconnected')).not.toBeInTheDocument();
130
+ });
131
+
132
+ it('shows Live during `connecting` and `initial`', () => {
133
+ const { rerender } = render(<CollaborationPanel {...defaultProps} state="connecting" />);
134
+ expect(screen.getByText('Live')).toBeInTheDocument();
135
+ rerender(<CollaborationPanel {...defaultProps} state="initial" />);
136
+ expect(screen.getByText('Live')).toBeInTheDocument();
137
+ });
138
+
139
+ it('shows Disconnected on `degraded` (sustained disconnect)', () => {
140
+ render(<CollaborationPanel {...defaultProps} state="degraded" />);
141
+ expect(screen.getByText('Disconnected')).toBeInTheDocument();
142
+ });
143
+
144
+ it('shows Disconnected on `closed` (terminal)', () => {
145
+ render(<CollaborationPanel {...defaultProps} state="closed" />);
146
+ expect(screen.getByText('Disconnected')).toBeInTheDocument();
147
+ });
117
148
  });
118
149
 
119
150
  describe('Event Count', () => {
120
151
  it('should not show event count when zero', () => {
121
- render(<CollaborationPanel {...defaultProps} isConnected={true} eventCount={0} />);
152
+ render(<CollaborationPanel {...defaultProps} state="open" eventCount={0} />);
122
153
 
123
154
  expect(screen.queryByText(/event/i)).not.toBeInTheDocument();
124
155
  });
125
156
 
126
157
  it('should show event count when connected and greater than zero', () => {
127
- render(<CollaborationPanel {...defaultProps} isConnected={true} eventCount={5} />);
158
+ render(<CollaborationPanel {...defaultProps} state="open" eventCount={5} />);
128
159
 
129
160
  expect(screen.getByText(/event/i)).toBeInTheDocument();
130
161
  });
131
162
 
132
163
  it('should not show event count when disconnected', () => {
133
- render(<CollaborationPanel {...defaultProps} isConnected={false} eventCount={5} />);
164
+ render(<CollaborationPanel {...defaultProps} state="degraded" eventCount={5} />);
134
165
 
135
166
  expect(screen.queryByText(/event/i)).not.toBeInTheDocument();
136
167
  });
137
168
 
138
169
  it('should display correct event count', () => {
139
- render(<CollaborationPanel {...defaultProps} isConnected={true} eventCount={42} />);
170
+ render(<CollaborationPanel {...defaultProps} state="open" eventCount={42} />);
140
171
 
141
172
  // The translation will have ${count} in it
142
173
  expect(screen.getByText(/event/i)).toBeInTheDocument();
@@ -267,13 +298,13 @@ describe('CollaborationPanel Component', () => {
267
298
 
268
299
  describe('Real-time Status Messages', () => {
269
300
  it('should show "real-time active" when connected', () => {
270
- render(<CollaborationPanel {...defaultProps} isConnected={true} />);
301
+ render(<CollaborationPanel {...defaultProps} state="open" />);
271
302
 
272
303
  expect(screen.getByText('Real-time synchronization active')).toBeInTheDocument();
273
304
  });
274
305
 
275
306
  it('should show "reconnecting" when disconnected', () => {
276
- render(<CollaborationPanel {...defaultProps} isConnected={false} />);
307
+ render(<CollaborationPanel {...defaultProps} state="degraded" />);
277
308
 
278
309
  expect(screen.getByText('Reconnecting...')).toBeInTheDocument();
279
310
  });
@@ -281,11 +312,11 @@ describe('CollaborationPanel Component', () => {
281
312
 
282
313
  describe('Dynamic Updates', () => {
283
314
  it('should update when connection status changes', () => {
284
- const { rerender } = render(<CollaborationPanel {...defaultProps} isConnected={false} />);
315
+ const { rerender } = render(<CollaborationPanel {...defaultProps} state="degraded" />);
285
316
 
286
317
  expect(screen.getByText('Disconnected')).toBeInTheDocument();
287
318
 
288
- rerender(<CollaborationPanel {...defaultProps} isConnected={true} />);
319
+ rerender(<CollaborationPanel {...defaultProps} state="open" />);
289
320
 
290
321
  expect(screen.getByText('Live')).toBeInTheDocument();
291
322
  expect(screen.queryByText('Disconnected')).not.toBeInTheDocument();
@@ -293,12 +324,12 @@ describe('CollaborationPanel Component', () => {
293
324
 
294
325
  it('should update when event count changes', () => {
295
326
  const { rerender } = render(
296
- <CollaborationPanel {...defaultProps} isConnected={true} eventCount={5} />
327
+ <CollaborationPanel {...defaultProps} state="open" eventCount={5} />
297
328
  );
298
329
 
299
330
  expect(screen.getByText(/event/i)).toBeInTheDocument();
300
331
 
301
- rerender(<CollaborationPanel {...defaultProps} isConnected={true} eventCount={10} />);
332
+ rerender(<CollaborationPanel {...defaultProps} state="open" eventCount={10} />);
302
333
 
303
334
  expect(screen.getByText(/event/i)).toBeInTheDocument();
304
335
  });
@@ -371,7 +402,7 @@ describe('CollaborationPanel Component', () => {
371
402
  it('should handle very large event counts', () => {
372
403
  expect(() => {
373
404
  render(
374
- <CollaborationPanel {...defaultProps} isConnected={true} eventCount={999999} />
405
+ <CollaborationPanel {...defaultProps} state="open" eventCount={999999} />
375
406
  );
376
407
  }).not.toThrow();
377
408
  });
@@ -379,7 +410,7 @@ describe('CollaborationPanel Component', () => {
379
410
  it('should handle negative event counts', () => {
380
411
  expect(() => {
381
412
  render(
382
- <CollaborationPanel {...defaultProps} isConnected={true} eventCount={-5} />
413
+ <CollaborationPanel {...defaultProps} state="open" eventCount={-5} />
383
414
  );
384
415
  }).not.toThrow();
385
416
  });
@@ -432,7 +463,7 @@ describe('CollaborationPanel Component', () => {
432
463
  });
433
464
 
434
465
  it('should have visible status indicators', () => {
435
- const { container } = render(<CollaborationPanel {...defaultProps} isConnected={true} />);
466
+ const { container } = render(<CollaborationPanel {...defaultProps} state="open" />);
436
467
 
437
468
  // Should have a visible status dot
438
469
  const indicator = container.querySelector('.semiont-collaboration-panel__dot');
@@ -8,7 +8,7 @@ import { CommentEntry } from '../CommentEntry';
8
8
  import type { components } from '@semiont/core';
9
9
  import type { EventBus } from "@semiont/core"
10
10
 
11
- type Annotation = components['schemas']['Annotation'];
11
+ import type { Annotation } from '@semiont/core';
12
12
 
13
13
  // Mock TranslationContext
14
14
  vi.mock('../../../../contexts/TranslationContext', () => ({
@@ -24,8 +24,8 @@ vi.mock('../../../../contexts/TranslationContext', () => ({
24
24
  }));
25
25
 
26
26
  // Mock @semiont/api-client utilities
27
- vi.mock('@semiont/api-client', async () => {
28
- const actual = await vi.importActual('@semiont/api-client');
27
+ vi.mock('@semiont/core', async () => {
28
+ const actual = await vi.importActual('@semiont/core');
29
29
  return {
30
30
  ...actual,
31
31
  getCommentText: vi.fn(),
@@ -33,7 +33,7 @@ vi.mock('@semiont/api-client', async () => {
33
33
  };
34
34
  });
35
35
 
36
- import { getCommentText, getAnnotationExactText } from '@semiont/api-client';
36
+ import { getCommentText, getAnnotationExactText } from '@semiont/core';
37
37
  import type { MockedFunction } from 'vitest';
38
38
 
39
39
  const mockGetCommentText = getCommentText as MockedFunction<typeof getCommentText>;
@@ -5,10 +5,10 @@ import { render, screen, fireEvent, waitFor } from '@testing-library/react';
5
5
  import userEvent from '@testing-library/user-event';
6
6
  import '@testing-library/jest-dom';
7
7
  import { CommentsPanel } from '../CommentsPanel';
8
- import { EventBusProvider, useEventBus } from '../../../../contexts/EventBusContext';
9
- import type { components } from '@semiont/core';
8
+ import type { components, EventBus } from '@semiont/core';
9
+ import { createTestSemiontWrapper } from '../../../../test-utils';
10
10
 
11
- type Annotation = components['schemas']['Annotation'];
11
+ import type { Annotation } from '@semiont/core';
12
12
 
13
13
  // Composition-based event tracker
14
14
  interface TrackedEvent {
@@ -18,59 +18,27 @@ interface TrackedEvent {
18
18
 
19
19
  function createEventTracker() {
20
20
  const events: TrackedEvent[] = [];
21
-
22
- function EventTrackingWrapper({ children }: { children: React.ReactNode }) {
23
- const eventBus = useEventBus();
24
-
25
- React.useEffect(() => {
26
- const handlers: Array<() => void> = [];
27
-
28
- const trackEvent = (eventName: string) => (payload: any) => {
29
- events.push({ event: eventName, payload });
30
- };
31
-
32
- const panelEvents = ['mark:submit'] as const;
33
-
34
- panelEvents.forEach(eventName => {
35
- const handler = trackEvent(eventName);
36
- const subscription = eventBus.get(eventName).subscribe(handler);
37
- handlers.push(subscription);
38
- });
39
-
40
- return () => {
41
- handlers.forEach(sub => sub.unsubscribe());
42
- };
43
- }, [eventBus]);
44
-
45
- return <>{children}</>;
46
- }
47
-
48
21
  return {
49
- EventTrackingWrapper,
50
22
  events,
51
- clear: () => {
52
- events.length = 0;
23
+ clear: () => { events.length = 0; },
24
+ _attach(eventBus: EventBus) {
25
+ const panelEvents = ['mark:submit'] as const;
26
+ panelEvents.forEach((eventName) => {
27
+ eventBus.get(eventName).subscribe((payload: any) => {
28
+ events.push({ event: eventName, payload });
29
+ });
30
+ });
53
31
  },
54
32
  };
55
33
  }
56
34
 
57
- // Helper to render with EventBusProvider
58
35
  const renderWithEventBus = (component: React.ReactElement, tracker?: ReturnType<typeof createEventTracker>) => {
59
- if (tracker) {
60
- return render(
61
- <EventBusProvider>
62
- <tracker.EventTrackingWrapper>
63
- {component}
64
- </tracker.EventTrackingWrapper>
65
- </EventBusProvider>
66
- );
67
- }
68
-
69
- return render(
70
- <EventBusProvider>
71
- {component}
72
- </EventBusProvider>
36
+ const { SemiontWrapper, eventBus } = createTestSemiontWrapper();
37
+ if (tracker) tracker._attach(eventBus);
38
+ const Wrapper = ({ children }: { children: React.ReactNode }) => (
39
+ <SemiontWrapper>{children}</SemiontWrapper>
73
40
  );
41
+ return render(component, { wrapper: Wrapper });
74
42
  };
75
43
 
76
44
  // Mock TranslationContext
@@ -89,8 +57,8 @@ vi.mock('../../../../contexts/TranslationContext', () => ({
89
57
  }));
90
58
 
91
59
  // Mock @semiont/api-client utilities
92
- vi.mock('@semiont/api-client', async () => {
93
- const actual = await vi.importActual('@semiont/api-client');
60
+ vi.mock('@semiont/core', async () => {
61
+ const actual = await vi.importActual('@semiont/core');
94
62
  return {
95
63
  ...actual,
96
64
  getTextPositionSelector: vi.fn(),
@@ -116,8 +84,7 @@ vi.mock('../CommentEntry', () => ({
116
84
  ),
117
85
  }));
118
86
 
119
- import { getTextPositionSelector, getTargetSelector } from '@semiont/api-client';
120
-
87
+ import { getTextPositionSelector, getTargetSelector } from '@semiont/core';
121
88
  const mockGetTextPositionSelector = getTextPositionSelector as MockedFunction<typeof getTextPositionSelector>;
122
89
  const mockGetTargetSelector = getTargetSelector as MockedFunction<typeof getTargetSelector>;
123
90
 
@@ -270,9 +237,7 @@ describe('CommentsPanel Component', () => {
270
237
  ];
271
238
 
272
239
  rerender(
273
- <EventBusProvider>
274
- <CommentsPanel {...defaultProps} annotations={updatedComments} />
275
- </EventBusProvider>
240
+ <CommentsPanel {...defaultProps} annotations={updatedComments} />
276
241
  );
277
242
 
278
243
  const comments = screen.getAllByTestId(/comment-/);
@@ -600,9 +565,7 @@ describe('CommentsPanel Component', () => {
600
565
  createMockComment(`${j + 1}`, j * 10, (j + 1) * 10)
601
566
  );
602
567
  rerender(
603
- <EventBusProvider>
604
- <CommentsPanel {...defaultProps} annotations={comments} />
605
- </EventBusProvider>
568
+ <CommentsPanel {...defaultProps} annotations={comments} />
606
569
  );
607
570
  }
608
571
 
@@ -617,9 +580,7 @@ describe('CommentsPanel Component', () => {
617
580
  expect(screen.getAllByTestId(/comment-/)).toHaveLength(3);
618
581
 
619
582
  rerender(
620
- <EventBusProvider>
621
- <CommentsPanel {...defaultProps} annotations={mockComments.single} />
622
- </EventBusProvider>
583
+ <CommentsPanel {...defaultProps} annotations={mockComments.single} />
623
584
  );
624
585
 
625
586
  expect(screen.getAllByTestId(/comment-/)).toHaveLength(1);
@@ -6,18 +6,18 @@ import { renderWithProviders } from '../../../../test-utils';
6
6
  import userEvent from '@testing-library/user-event';
7
7
  import type { components } from '@semiont/core';
8
8
 
9
- type Annotation = components['schemas']['Annotation'];
9
+ import type { Annotation } from '@semiont/core';
10
10
 
11
11
  // Mock @semiont/api-client
12
- vi.mock('@semiont/api-client', async () => {
13
- const actual = await vi.importActual('@semiont/api-client');
12
+ vi.mock('@semiont/core', async () => {
13
+ const actual = await vi.importActual('@semiont/core');
14
14
  return {
15
15
  ...actual,
16
16
  getAnnotationExactText: vi.fn(),
17
17
  };
18
18
  });
19
19
 
20
- import { getAnnotationExactText } from '@semiont/api-client';
20
+ import { getAnnotationExactText } from '@semiont/core';
21
21
  import type { MockedFunction } from 'vitest';
22
22
  import { HighlightEntry } from '../HighlightEntry';
23
23
 
@@ -18,7 +18,7 @@ import { renderWithProviders } from '../../../../test-utils';
18
18
  import { HighlightPanel } from '../HighlightPanel';
19
19
  import type { components } from '@semiont/core';
20
20
 
21
- type Annotation = components['schemas']['Annotation'];
21
+ import type { Annotation } from '@semiont/core';
22
22
 
23
23
  // Mock translations - simulates useTranslations('HighlightPanel')
24
24
  // The mock receives keys like 'title', 'noHighlights', etc. and returns translated strings
@@ -57,7 +57,6 @@ describe('HighlightPanel + AssistSection Integration', () => {
57
57
  {
58
58
  id: 'highlight-1',
59
59
  motivation: 'highlighting',
60
- body: [],
61
60
  target: {
62
61
  source: 'resource-1',
63
62
  selector: {
@@ -5,10 +5,10 @@ import '@testing-library/jest-dom';
5
5
  import { renderWithProviders } from '../../../../test-utils';
6
6
  import userEvent from '@testing-library/user-event';
7
7
  import type { components } from '@semiont/core';
8
- import { SemiontApiClient } from '@semiont/api-client';
8
+ import { BindNamespace } from '@semiont/sdk';
9
9
  import type { RouteBuilder } from '../../../../contexts/RoutingContext';
10
10
 
11
- type Annotation = components['schemas']['Annotation'];
11
+ import type { Annotation } from '@semiont/core';
12
12
 
13
13
  // Stable mock functions defined outside vi.mock to avoid re-render loops
14
14
  const mockGetAnnotationExactText = vi.fn();
@@ -21,8 +21,8 @@ const mockGetEntityTypes = vi.fn();
21
21
  const mockNavigate = vi.fn();
22
22
  const mockHoverProps = { onMouseEnter: vi.fn(), onMouseLeave: vi.fn() };
23
23
 
24
- vi.mock('@semiont/api-client', async () => {
25
- const actual = await vi.importActual('@semiont/api-client');
24
+ vi.mock('@semiont/core', async () => {
25
+ const actual = await vi.importActual('@semiont/core');
26
26
  return {
27
27
  ...actual,
28
28
  getAnnotationExactText: (...args: unknown[]) => mockGetAnnotationExactText(...args),
@@ -46,7 +46,7 @@ vi.mock('../../../../hooks/useObservableBrowse', () => ({
46
46
  useObservableExternalNavigation: () => mockNavigate,
47
47
  }));
48
48
 
49
- vi.mock('../../../../hooks/useBeckonFlow', () => ({
49
+ vi.mock('../../../../hooks/useHoverEmitter', () => ({
50
50
  useHoverEmitter: () => mockHoverProps,
51
51
  }));
52
52
 
@@ -308,7 +308,7 @@ describe('ReferenceEntry', () => {
308
308
  mockIsBodyResolved.mockReturnValue(true);
309
309
  mockGetBodySource.mockReturnValue('linked-doc');
310
310
 
311
- const bindSpy = vi.spyOn(SemiontApiClient.prototype, 'bindAnnotation').mockResolvedValue({ correlationId: 'c1' });
311
+ const bindSpy = vi.spyOn(BindNamespace.prototype, 'body').mockResolvedValue(undefined);
312
312
 
313
313
  const { container } = renderWithProviders(
314
314
  <ReferenceEntry {...defaultProps} annotateMode={true} />,
@@ -320,8 +320,7 @@ describe('ReferenceEntry', () => {
320
320
  expect(bindSpy).toHaveBeenCalledWith(
321
321
  'resource-1',
322
322
  'ref-1',
323
- { operations: [{ op: 'remove', item: { type: 'SpecificResource', source: 'linked-doc', purpose: 'linking' } }] },
324
- expect.anything(),
323
+ [{ op: 'remove', item: { type: 'SpecificResource', source: 'linked-doc', purpose: 'linking' } }],
325
324
  );
326
325
 
327
326
  bindSpy.mockRestore();
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Triangulation test for the VM → useObservable → prop → chip render chain.
3
+ *
4
+ * Written after an e2e failure (test 05) where `ReferencesPanel` rendered
5
+ * "No entity types available" even though the client provably received a
6
+ * 9-string entity-types array from the backend.
7
+ *
8
+ * This test closes the Layer 5-6 gap: the existing `ResourceViewerPage.test.tsx`
9
+ * stubs `UnifiedAnnotationsPanel`/`ReferencesPanel` as `<div data-testid>`s,
10
+ * so it never verifies that an observable emitting [9 strings] actually
11
+ * produces 9 chips in the DOM. This test does.
12
+ *
13
+ * If this test passes and the e2e still fails, the bug is further upstream
14
+ * (BrowseNamespace wiring, multiple ApiClient instances, SSE delivery).
15
+ * If it fails, the bug is here in component-land.
16
+ */
17
+
18
+ import { describe, it, expect, vi } from 'vitest';
19
+ import React from 'react';
20
+ import { render, screen, act } from '@testing-library/react';
21
+ import '@testing-library/jest-dom';
22
+ import { BehaviorSubject } from 'rxjs';
23
+ import { ReferencesPanel } from '../ReferencesPanel';
24
+ import { createTestSemiontWrapper } from '../../../../test-utils';
25
+ import { useObservable } from '../../../../hooks/useObservable';
26
+
27
+ // Match ReferencesPanel.test.tsx's i18n mocking so the test doesn't
28
+ // depend on a real translation setup.
29
+ vi.mock('../../../../contexts/TranslationContext', () => ({
30
+ useTranslations: () => (key: string) => {
31
+ const map: Record<string, string> = {
32
+ title: 'References',
33
+ annotateReferences: 'Annotate References',
34
+ entityTypesOptional: 'Entity types',
35
+ noEntityTypes: 'No entity types available',
36
+ selectEntityTypes: 'Select entity types',
37
+ includeDescriptiveReferences: 'Include descriptive references',
38
+ cancel: 'Cancel',
39
+ createReference: 'Create Reference',
40
+ outgoingReferences: 'Outgoing References',
41
+ incomingReferences: 'Incoming References',
42
+ noIncomingReferences: 'No incoming references',
43
+ fragmentSelected: 'Fragment selected',
44
+ annotate: 'Annotate',
45
+ start: 'Start',
46
+ };
47
+ return map[key] ?? key;
48
+ },
49
+ }));
50
+
51
+ vi.mock('../AssistSection', () => ({
52
+ AssistSection: () => null,
53
+ AnnotateReferencesProgressWidget: () => null,
54
+ }));
55
+
56
+ const NINE_TYPES = [
57
+ 'Author', 'Concept', 'Date', 'Event', 'Location',
58
+ 'Organization', 'Person', 'Product', 'Technology',
59
+ ];
60
+
61
+ const MockLink: React.FC<{ href?: string; children?: React.ReactNode }> = ({ children }) => <>{children}</>;
62
+ const mockRoutes = { resourceDetail: (id: string) => `/resource/${id}` } as any;
63
+
64
+ /**
65
+ * Thin harness: subscribes to a BehaviorSubject via useObservable (the
66
+ * same hook ResourceViewerPage uses for vm.entityTypes$) and forwards
67
+ * its value into ReferencesPanel as `allEntityTypes`.
68
+ */
69
+ function ObservableHarness({ source$ }: { source$: BehaviorSubject<string[]> }) {
70
+ const entityTypes = useObservable(source$) ?? [];
71
+ return (
72
+ <ReferencesPanel
73
+ annotations={[]}
74
+ isAssisting={false}
75
+ progress={null}
76
+ annotateMode={true}
77
+ Link={MockLink}
78
+ routes={mockRoutes}
79
+ allEntityTypes={entityTypes}
80
+ pendingAnnotation={{
81
+ motivation: 'linking',
82
+ selector: { type: 'TextQuoteSelector', exact: 'te' },
83
+ } as any}
84
+ />
85
+ );
86
+ }
87
+
88
+ const renderWithBus = (ui: React.ReactElement) => {
89
+ const { SemiontWrapper } = createTestSemiontWrapper();
90
+ return render(<SemiontWrapper>{ui}</SemiontWrapper>);
91
+ };
92
+
93
+ describe('Layer 5-6 — VM observable → useObservable → ReferencesPanel chips', () => {
94
+ it('an observable seeded with [9 strings] renders 9 pending-reference chips', async () => {
95
+ const source$ = new BehaviorSubject<string[]>(NINE_TYPES);
96
+ renderWithBus(<ObservableHarness source$={source$} />);
97
+
98
+ // Wait for useEffect in useObservable to run and commit the value.
99
+ const chips = await screen.findAllByRole('button', { name: (_, el) =>
100
+ el.classList.contains('semiont-tag-selector__item') ?? false,
101
+ });
102
+ expect(chips).toHaveLength(NINE_TYPES.length);
103
+ });
104
+
105
+ it('transition [] → [9 strings] re-renders with 9 chips', async () => {
106
+ // This is the production timeline: the Cache emits undefined (mapped
107
+ // to []) first, then the 9-string array once fetch resolves. The
108
+ // prop chain must survive this transition.
109
+ const source$ = new BehaviorSubject<string[]>([]);
110
+ renderWithBus(<ObservableHarness source$={source$} />);
111
+
112
+ // Initially: no chips (the gate is allEntityTypes.length > 0).
113
+ expect(document.querySelectorAll('.semiont-tag-selector__item').length).toBe(0);
114
+
115
+ await act(async () => {
116
+ source$.next(NINE_TYPES);
117
+ // Let useObservable's setState flush.
118
+ await new Promise((r) => setTimeout(r, 0));
119
+ });
120
+
121
+ expect(document.querySelectorAll('.semiont-tag-selector__item').length).toBe(NINE_TYPES.length);
122
+ });
123
+
124
+ it('[9 strings] with duplicate emissions (simulating SSE-duplicate deliveries) renders 9 chips', async () => {
125
+ // The failing e2e run showed the same correlationId delivered 3x due
126
+ // to concurrent SSE streams. Same data, but multiple BehaviorSubject
127
+ // writes. Must not clobber the render.
128
+ const source$ = new BehaviorSubject<string[]>([]);
129
+ renderWithBus(<ObservableHarness source$={source$} />);
130
+
131
+ await act(async () => {
132
+ source$.next(NINE_TYPES);
133
+ source$.next([...NINE_TYPES]); // same content, new reference
134
+ source$.next([...NINE_TYPES]);
135
+ await new Promise((r) => setTimeout(r, 0));
136
+ });
137
+
138
+ expect(document.querySelectorAll('.semiont-tag-selector__item').length).toBe(NINE_TYPES.length);
139
+ });
140
+
141
+ it('no regression: [] still renders "No entity types available"', () => {
142
+ // Confirms the control case: an empty observable does render the
143
+ // message the failing e2e saw. Guards against the test passing
144
+ // trivially because of a selector bug.
145
+ const source$ = new BehaviorSubject<string[]>([]);
146
+ renderWithBus(<ObservableHarness source$={source$} />);
147
+
148
+ // There are two such text nodes in the panel (pending prompt + assist
149
+ // section), but both correspond to the same allEntityTypes=[] state.
150
+ const msg = screen.queryAllByText(/no entity types available/i);
151
+ expect(msg.length).toBeGreaterThan(0);
152
+ });
153
+ });