@semiont/react-ui 0.4.19 → 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
@@ -2,21 +2,22 @@
2
2
 
3
3
  import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
4
4
  import { useTranslations } from '../../../contexts/TranslationContext';
5
- import { useEventBus } from '../../../contexts/EventBusContext';
5
+ import { useSemiont } from '../../../session/SemiontProvider';
6
+ import { useObservable } from '../../../hooks/useObservable';
6
7
  import { useEventSubscriptions } from '../../../contexts/useEventSubscription';
7
8
  import type { RouteBuilder, LinkComponentProps } from '../../../contexts/RoutingContext';
8
9
  import { AnnotateReferencesProgressWidget } from '../../AnnotateReferencesProgressWidget';
9
10
  import { ReferenceEntry } from './ReferenceEntry';
10
- import type { components, paths, Selector } from '@semiont/core';
11
+ import type { components, Selector } from '@semiont/core';
11
12
  import { getTextPositionSelector, getTargetSelector } from '@semiont/api-client';
12
13
  import { PanelHeader } from './PanelHeader';
13
14
  import './ReferencesPanel.css';
14
- import type { MarkProgress } from '@semiont/core';
15
+
16
+ type JobProgress = components['schemas']['JobProgress'];
15
17
 
16
18
  type Annotation = components['schemas']['Annotation'];
17
19
  type Motivation = components['schemas']['Motivation'];
18
- type ResponseContent<T> = T extends { responses: { 200: { content: { 'application/json': infer R } } } } ? R : never;
19
- type ReferencedBy = ResponseContent<paths['/resources/{id}/referenced-by']['get']>['referencedBy'][number];
20
+ type ReferencedBy = components['schemas']['GetReferencedByResponse']['referencedBy'][number];
20
21
 
21
22
  // Unified pending annotation type
22
23
  interface PendingAnnotation {
@@ -45,7 +46,7 @@ interface Props {
45
46
  // Generic panel props
46
47
  annotations?: Annotation[];
47
48
  isAssisting: boolean;
48
- progress: MarkProgress | null;
49
+ progress: JobProgress | null;
49
50
  annotateMode?: boolean;
50
51
  Link: React.ComponentType<LinkComponentProps>;
51
52
  routes: RouteBuilder;
@@ -86,7 +87,7 @@ export function ReferencesPanel({
86
87
  hoveredAnnotationId,
87
88
  }: Props) {
88
89
  const t = useTranslations('ReferencesPanel');
89
- const eventBus = useEventBus();
90
+ const session = useObservable(useSemiont().activeSession$);
90
91
  const [selectedEntityTypes, setSelectedEntityTypes] = useState<string[]>([]);
91
92
  const [lastAnnotationLog, setLastDetectionLog] = useState<Array<{ entityType: string; foundCount: number }> | null>(null);
92
93
  const [pendingEntityTypes, setPendingEntityTypes] = useState<string[]>([]);
@@ -202,7 +203,7 @@ export function ReferencesPanel({
202
203
  // Clear log when starting new annotation
203
204
  const handleAssist = () => {
204
205
  setLastDetectionLog(null);
205
- eventBus.get('mark:assist-request').next({
206
+ session?.client.emit('mark:assist-request', {
206
207
  motivation: 'linking',
207
208
  options: {
208
209
  entityTypes: selectedEntityTypes,
@@ -245,7 +246,7 @@ export function ReferencesPanel({
245
246
  const handleCreateReference = () => {
246
247
  if (pendingAnnotation) {
247
248
  const entityType = pendingEntityTypes.join(',') || undefined;
248
- eventBus.get('mark:submit').next({
249
+ session?.client.emit('mark:submit', {
249
250
  motivation: 'linking',
250
251
  selector: pendingAnnotation.selector,
251
252
  body: entityType ? [{ type: 'TextualBody', value: entityType, purpose: 'tagging' }] : [],
@@ -260,14 +261,14 @@ export function ReferencesPanel({
260
261
 
261
262
  const handleEscape = (e: KeyboardEvent) => {
262
263
  if (e.key === 'Escape') {
263
- eventBus.get('mark:cancel-pending').next(undefined);
264
+ session?.client.emit('mark:cancel-pending', undefined);
264
265
  setPendingEntityTypes([]);
265
266
  }
266
267
  };
267
268
 
268
269
  document.addEventListener('keydown', handleEscape);
269
270
  return () => document.removeEventListener('keydown', handleEscape);
270
- }, [pendingAnnotation]);
271
+ }, [pendingAnnotation, session]);
271
272
 
272
273
  return (
273
274
  <div className="semiont-panel">
@@ -312,7 +313,7 @@ export function ReferencesPanel({
312
313
  <div className="semiont-annotation-prompt__actions">
313
314
  <button
314
315
  onClick={() => {
315
- eventBus.get('mark:cancel-pending').next(undefined);
316
+ session?.client.emit('mark:cancel-pending', undefined);
316
317
  setPendingEntityTypes([]);
317
318
  }}
318
319
  className="semiont-button semiont-button--secondary"
@@ -1,7 +1,8 @@
1
1
  'use client';
2
2
 
3
3
  import { useTranslations } from '../../../contexts/TranslationContext';
4
- import { useEventBus } from '../../../contexts/EventBusContext';
4
+ import { useSemiont } from '../../../session/SemiontProvider';
5
+ import { useObservable } from '../../../hooks/useObservable';
5
6
  import { formatLocaleDisplay } from '@semiont/api-client';
6
7
  import { resourceId as makeResourceId, type components } from '@semiont/core';
7
8
  import './ResourceInfoPanel.css';
@@ -47,7 +48,7 @@ export function ResourceInfoPanel({
47
48
  generator,
48
49
  }: Props) {
49
50
  const t = useTranslations('ResourceInfoPanel');
50
- const eventBus = useEventBus();
51
+ const session = useObservable(useSemiont().activeSession$);
51
52
 
52
53
  return (
53
54
  <div className="semiont-resource-info-panel">
@@ -148,7 +149,7 @@ export function ResourceInfoPanel({
148
149
  <button
149
150
  key={id}
150
151
  className="semiont-resource-info-panel__link"
151
- onClick={() => eventBus.get('browse:reference-navigate').next({ resourceId: id })}
152
+ onClick={() => session?.client.emit('browse:reference-navigate', { resourceId: id })}
152
153
  >
153
154
  {i > 0 && ', '}{id}
154
155
  </button>
@@ -191,7 +192,7 @@ export function ResourceInfoPanel({
191
192
  {/* Clone Action */}
192
193
  <div className="semiont-resource-info-panel__action-section">
193
194
  <button
194
- onClick={() => eventBus.get('yield:clone').next(undefined)}
195
+ onClick={() => session?.client.emit('yield:clone', undefined)}
195
196
  className="semiont-resource-button semiont-resource-button--secondary"
196
197
  >
197
198
  🔗 {t('clone')}
@@ -206,7 +207,7 @@ export function ResourceInfoPanel({
206
207
  {isArchived ? (
207
208
  <>
208
209
  <button
209
- onClick={() => eventBus.get('mark:unarchive').next({ resourceId: makeResourceId(resourceId) })}
210
+ onClick={() => session?.client.emit('mark:unarchive', { resourceId: makeResourceId(resourceId) })}
210
211
  className="semiont-resource-button semiont-resource-button--secondary"
211
212
  >
212
213
  📤 {t('unarchive')}
@@ -218,7 +219,7 @@ export function ResourceInfoPanel({
218
219
  ) : (
219
220
  <>
220
221
  <button
221
- onClick={() => eventBus.get('mark:archive').next({ resourceId: makeResourceId(resourceId) })}
222
+ onClick={() => session?.client.emit('mark:archive', { resourceId: makeResourceId(resourceId) })}
222
223
  className="semiont-resource-button semiont-resource-button--archive"
223
224
  >
224
225
  📦 {t('archive')}
@@ -5,8 +5,9 @@ import type { components } from '@semiont/core';
5
5
  import { getAnnotationExactText } from '@semiont/api-client';
6
6
  import { getTagCategory, getTagSchemaId } from '@semiont/ontology';
7
7
  import { getTagSchema } from '../../../lib/tag-schemas';
8
- import { useEventBus } from '../../../contexts/EventBusContext';
9
- import { useHoverEmitter } from '../../../hooks/useBeckonFlow';
8
+ import { useSemiont } from '../../../session/SemiontProvider';
9
+ import { useObservable } from '../../../hooks/useObservable';
10
+ import { useHoverEmitter } from '../../../hooks/useHoverEmitter';
10
11
 
11
12
  type Annotation = components['schemas']['Annotation'];
12
13
 
@@ -23,7 +24,7 @@ export function TagEntry({
23
24
  isHovered = false,
24
25
  ref,
25
26
  }: TagEntryProps) {
26
- const eventBus = useEventBus();
27
+ const session = useObservable(useSemiont().activeSession$);
27
28
  const hoverProps = useHoverEmitter(tag.id);
28
29
 
29
30
  const selectedText = getAnnotationExactText(tag);
@@ -35,7 +36,7 @@ export function TagEntry({
35
36
  <div
36
37
  ref={ref}
37
38
  onClick={() => {
38
- eventBus.get('browse:click').next({ annotationId: tag.id, motivation: tag.motivation });
39
+ session?.client.emit('browse:click', { annotationId: tag.id, motivation: tag.motivation });
39
40
  }}
40
41
  {...hoverProps}
41
42
  className={`semiont-annotation-entry${isHovered ? ' semiont-annotation-pulse' : ''}`}
@@ -2,7 +2,8 @@
2
2
 
3
3
  import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
4
4
  import { useTranslations } from '../../../contexts/TranslationContext';
5
- import { useEventBus } from '../../../contexts/EventBusContext';
5
+ import { useSemiont } from '../../../session/SemiontProvider';
6
+ import { useObservable } from '../../../hooks/useObservable';
6
7
  import { useEventSubscriptions } from '../../../contexts/useEventSubscription';
7
8
  import type { components, Selector } from '@semiont/core';
8
9
  import { getTextPositionSelector, getTargetSelector } from '@semiont/api-client';
@@ -13,6 +14,7 @@ import './TaggingPanel.css';
13
14
 
14
15
  type Annotation = components['schemas']['Annotation'];
15
16
  type Motivation = components['schemas']['Motivation'];
17
+ type JobProgress = components['schemas']['JobProgress'];
16
18
 
17
19
  // Unified pending annotation type
18
20
  interface PendingAnnotation {
@@ -41,15 +43,7 @@ interface TaggingPanelProps {
41
43
  annotations: Annotation[];
42
44
  annotateMode?: boolean;
43
45
  isAssisting?: boolean;
44
- progress?: {
45
- status: string;
46
- percentage?: number;
47
- currentCategory?: string;
48
- processedCategories?: number;
49
- totalCategories?: number;
50
- message?: string;
51
- requestParams?: Array<{ label: string; value: string }>;
52
- } | null;
46
+ progress?: JobProgress | null;
53
47
  pendingAnnotation: PendingAnnotation | null;
54
48
  scrollToAnnotationId?: string | null;
55
49
  onScrollCompleted?: () => void;
@@ -75,7 +69,7 @@ export function TaggingPanel({
75
69
  hoveredAnnotationId,
76
70
  }: TaggingPanelProps) {
77
71
  const t = useTranslations('TaggingPanel');
78
- const eventBus = useEventBus();
72
+ const session = useObservable(useSemiont().activeSession$);
79
73
  const [selectedSchemaId, setSelectedSchemaId] = useState<string>('legal-irac');
80
74
  const [selectedCategories, setSelectedCategories] = useState<Set<string>>(new Set());
81
75
  const [focusedAnnotationId, setFocusedAnnotationId] = useState<string | null>(null);
@@ -195,7 +189,7 @@ export function TaggingPanel({
195
189
 
196
190
  const handleAssist = () => {
197
191
  if (selectedCategories.size > 0) {
198
- eventBus.get('mark:assist-request').next({
192
+ session?.client.emit('mark:assist-request', {
199
193
  motivation: 'tagging',
200
194
  options: {
201
195
  schemaId: selectedSchemaId,
@@ -212,13 +206,13 @@ export function TaggingPanel({
212
206
 
213
207
  const handleEscape = (e: KeyboardEvent) => {
214
208
  if (e.key === 'Escape') {
215
- eventBus.get('mark:cancel-pending').next(undefined);
209
+ session?.client.emit('mark:cancel-pending', undefined);
216
210
  }
217
211
  };
218
212
 
219
213
  document.addEventListener('keydown', handleEscape);
220
214
  return () => document.removeEventListener('keydown', handleEscape);
221
- }, [pendingAnnotation]);
215
+ }, [pendingAnnotation, session]);
222
216
 
223
217
  // Color schemes are now handled via CSS data attributes
224
218
 
@@ -274,7 +268,7 @@ export function TaggingPanel({
274
268
  className="semiont-select"
275
269
  onChange={(e) => {
276
270
  if (e.target.value && pendingAnnotation) {
277
- eventBus.get('mark:submit').next({
271
+ session?.client.emit('mark:submit', {
278
272
  motivation: 'tagging',
279
273
  selector: pendingAnnotation.selector,
280
274
  body: [
@@ -305,7 +299,7 @@ export function TaggingPanel({
305
299
  {/* Cancel button */}
306
300
  <div className="semiont-annotation-prompt__footer">
307
301
  <button
308
- onClick={() => eventBus.get('mark:cancel-pending').next(undefined)}
302
+ onClick={() => session?.client.emit('mark:cancel-pending', undefined)}
309
303
  className="semiont-button semiont-button--secondary"
310
304
  data-type="tag"
311
305
  >
@@ -2,7 +2,8 @@
2
2
 
3
3
  import React, { useState, useEffect } from 'react';
4
4
  import { useTranslations } from '../../../contexts/TranslationContext';
5
- import type { components, Selector, MarkProgress } from '@semiont/core';
5
+ import type { components, Selector } from '@semiont/core';
6
+ type JobProgress = components['schemas']['JobProgress'];
6
7
  import type { RouteBuilder, LinkComponentProps } from '../../../contexts/RoutingContext';
7
8
  import type { Annotator } from '../../../lib/annotation-registry';
8
9
  import { StatisticsPanel } from './StatisticsPanel';
@@ -47,7 +48,7 @@ interface UnifiedAnnotationsPanelProps {
47
48
 
48
49
  // Annotation assistance state (per motivation)
49
50
  assistingMotivation?: Motivation | null;
50
- progress?: MarkProgress | null;
51
+ progress?: JobProgress | null;
51
52
 
52
53
  // Unified pending annotation (for creating new annotations)
53
54
  pendingAnnotation: PendingAnnotation | null;
@@ -5,8 +5,8 @@ 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 { AssessmentPanel } from '../AssessmentPanel';
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
11
  type Annotation = components['schemas']['Annotation'];
12
12
 
@@ -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
@@ -108,8 +76,7 @@ vi.mock('../AssessmentEntry', () => ({
108
76
  ),
109
77
  }));
110
78
 
111
- // Mock AssistSection component - it will internally use the mocked useEventBus
112
- // Just render a simplified version
79
+ // Mock AssistSection component just render a simplified version.
113
80
  vi.mock('../AssistSection', () => ({
114
81
  AssistSection: ({ annotationType, isAssisting }: any) => (
115
82
  <div data-testid="detect-section">
@@ -380,7 +347,7 @@ describe('AssessmentPanel Component', () => {
380
347
  expect(tracker.events.some(e =>
381
348
  e.event === 'mark:submit' &&
382
349
  e.payload?.motivation === 'assessing' &&
383
- e.payload?.body?.[0]?.value === 'My assessment'
350
+ e.payload?.body?.value === 'My assessment'
384
351
  )).toBe(true);
385
352
  });
386
353
  });
@@ -421,8 +388,7 @@ describe('AssessmentPanel Component', () => {
421
388
  expect(tracker.events.some(e =>
422
389
  e.event === 'mark:submit' &&
423
390
  e.payload?.motivation === 'assessing' &&
424
- Array.isArray(e.payload?.body) &&
425
- e.payload.body.length === 0
391
+ e.payload?.body === undefined
426
392
  )).toBe(true);
427
393
  });
428
394
  });
@@ -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');