@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
@@ -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 { 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
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
@@ -270,9 +238,7 @@ describe('CommentsPanel Component', () => {
270
238
  ];
271
239
 
272
240
  rerender(
273
- <EventBusProvider>
274
- <CommentsPanel {...defaultProps} annotations={updatedComments} />
275
- </EventBusProvider>
241
+ <CommentsPanel {...defaultProps} annotations={updatedComments} />
276
242
  );
277
243
 
278
244
  const comments = screen.getAllByTestId(/comment-/);
@@ -600,9 +566,7 @@ describe('CommentsPanel Component', () => {
600
566
  createMockComment(`${j + 1}`, j * 10, (j + 1) * 10)
601
567
  );
602
568
  rerender(
603
- <EventBusProvider>
604
- <CommentsPanel {...defaultProps} annotations={comments} />
605
- </EventBusProvider>
569
+ <CommentsPanel {...defaultProps} annotations={comments} />
606
570
  );
607
571
  }
608
572
 
@@ -617,9 +581,7 @@ describe('CommentsPanel Component', () => {
617
581
  expect(screen.getAllByTestId(/comment-/)).toHaveLength(3);
618
582
 
619
583
  rerender(
620
- <EventBusProvider>
621
- <CommentsPanel {...defaultProps} annotations={mockComments.single} />
622
- </EventBusProvider>
584
+ <CommentsPanel {...defaultProps} annotations={mockComments.single} />
623
585
  );
624
586
 
625
587
  expect(screen.getAllByTestId(/comment-/)).toHaveLength(1);
@@ -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,7 +5,7 @@ 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/api-client';
9
9
  import type { RouteBuilder } from '../../../../contexts/RoutingContext';
10
10
 
11
11
  type Annotation = components['schemas']['Annotation'];
@@ -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
+ });
@@ -4,7 +4,8 @@ import { render, screen, fireEvent, waitFor } from '@testing-library/react';
4
4
  import userEvent from '@testing-library/user-event';
5
5
  import '@testing-library/jest-dom';
6
6
  import { ReferencesPanel } from '../ReferencesPanel';
7
- import { EventBusProvider, useEventBus } from '../../../../contexts/EventBusContext';
7
+ import type { EventBus } from '@semiont/core';
8
+ import { createTestSemiontWrapper } from '../../../../test-utils';
8
9
 
9
10
  // Composition-based event tracker
10
11
  interface TrackedEvent {
@@ -14,59 +15,27 @@ interface TrackedEvent {
14
15
 
15
16
  function createEventTracker() {
16
17
  const events: TrackedEvent[] = [];
17
-
18
- function EventTrackingWrapper({ children }: { children: React.ReactNode }) {
19
- const eventBus = useEventBus();
20
-
21
- React.useEffect(() => {
22
- const handlers: Array<() => void> = [];
23
-
24
- const trackEvent = (eventName: string) => (payload: any) => {
25
- events.push({ event: eventName, payload });
26
- };
27
-
28
- const panelEvents = ['mark:assist-request'] as const;
29
-
30
- panelEvents.forEach(eventName => {
31
- const handler = trackEvent(eventName);
32
- const subscription = eventBus.get(eventName).subscribe(handler);
33
- handlers.push(subscription);
34
- });
35
-
36
- return () => {
37
- handlers.forEach(sub => sub.unsubscribe());
38
- };
39
- }, [eventBus]);
40
-
41
- return <>{children}</>;
42
- }
43
-
44
18
  return {
45
- EventTrackingWrapper,
46
19
  events,
47
- clear: () => {
48
- events.length = 0;
20
+ clear: () => { events.length = 0; },
21
+ _attach(eventBus: EventBus) {
22
+ const panelEvents = ['mark:assist-request'] as const;
23
+ panelEvents.forEach((eventName) => {
24
+ eventBus.get(eventName).subscribe((payload: any) => {
25
+ events.push({ event: eventName, payload });
26
+ });
27
+ });
49
28
  },
50
29
  };
51
30
  }
52
31
 
53
- // Helper to render with EventBusProvider
54
32
  const renderWithEventBus = (component: React.ReactElement, tracker?: ReturnType<typeof createEventTracker>) => {
55
- if (tracker) {
56
- return render(
57
- <EventBusProvider>
58
- <tracker.EventTrackingWrapper>
59
- {component}
60
- </tracker.EventTrackingWrapper>
61
- </EventBusProvider>
62
- );
63
- }
64
-
65
- return render(
66
- <EventBusProvider>
67
- {component}
68
- </EventBusProvider>
33
+ const { SemiontWrapper, eventBus } = createTestSemiontWrapper();
34
+ if (tracker) tracker._attach(eventBus);
35
+ const Wrapper = ({ children }: { children: React.ReactNode }) => (
36
+ <SemiontWrapper>{children}</SemiontWrapper>
69
37
  );
38
+ return render(component, { wrapper: Wrapper });
70
39
  };
71
40
 
72
41
  // Mock TranslationContext
@@ -344,26 +313,22 @@ describe('ReferencesPanel Component', () => {
344
313
 
345
314
  // Simulate detection starting
346
315
  rerender(
347
- <EventBusProvider>
348
- <ReferencesPanel
349
- {...defaultProps}
350
- isAssisting={true}
351
- progress={{ completedEntityTypes: [] }}
352
- />
353
- </EventBusProvider>
316
+ <ReferencesPanel
317
+ {...defaultProps}
318
+ isAssisting={true}
319
+ progress={{ completedEntityTypes: [] }}
320
+ />
354
321
  );
355
322
 
356
323
  // Simulate detection completing
357
324
  rerender(
358
- <EventBusProvider>
359
- <ReferencesPanel
360
- {...defaultProps}
361
- isAssisting={false}
362
- progress={{
363
- completedEntityTypes: [{ entityType: 'Person', foundCount: 5 }],
364
- }}
365
- />
366
- </EventBusProvider>
325
+ <ReferencesPanel
326
+ {...defaultProps}
327
+ isAssisting={false}
328
+ progress={{
329
+ completedEntityTypes: [{ entityType: 'Person', foundCount: 5 }],
330
+ }}
331
+ />
367
332
  );
368
333
 
369
334
  // UI should reset but we can't directly test internal state
@@ -473,13 +438,11 @@ describe('ReferencesPanel Component', () => {
473
438
 
474
439
  // Parent clears progress after completion
475
440
  rerender(
476
- <EventBusProvider>
477
- <ReferencesPanel
478
- {...defaultProps}
479
- isAssisting={false}
480
- progress={null}
481
- />
482
- </EventBusProvider>
441
+ <ReferencesPanel
442
+ {...defaultProps}
443
+ isAssisting={false}
444
+ progress={null}
445
+ />
483
446
  );
484
447
 
485
448
  expect(screen.getByText('Person:')).toBeInTheDocument();
@@ -498,9 +461,7 @@ describe('ReferencesPanel Component', () => {
498
461
  );
499
462
 
500
463
  rerender(
501
- <EventBusProvider>
502
- <ReferencesPanel {...defaultProps} isAssisting={false} progress={null} />
503
- </EventBusProvider>
464
+ <ReferencesPanel {...defaultProps} isAssisting={false} progress={null} />
504
465
  );
505
466
  expect(screen.getByText(/Found.*5/i)).toBeInTheDocument();
506
467
  });
@@ -517,9 +478,7 @@ describe('ReferencesPanel Component', () => {
517
478
  );
518
479
 
519
480
  rerender(
520
- <EventBusProvider>
521
- <ReferencesPanel {...defaultProps} isAssisting={false} progress={null} />
522
- </EventBusProvider>
481
+ <ReferencesPanel {...defaultProps} isAssisting={false} progress={null} />
523
482
  );
524
483
  expect(screen.getByText('✓')).toBeInTheDocument();
525
484
  });
@@ -536,9 +495,7 @@ describe('ReferencesPanel Component', () => {
536
495
  );
537
496
 
538
497
  rerender(
539
- <EventBusProvider>
540
- <ReferencesPanel {...defaultProps} isAssisting={false} progress={null} />
541
- </EventBusProvider>
498
+ <ReferencesPanel {...defaultProps} isAssisting={false} progress={null} />
542
499
  );
543
500
 
544
501
  // Should show both the completed log AND the selection UI
@@ -558,9 +515,7 @@ describe('ReferencesPanel Component', () => {
558
515
  );
559
516
 
560
517
  rerender(
561
- <EventBusProvider>
562
- <ReferencesPanel {...defaultProps} isAssisting={false} progress={null} />
563
- </EventBusProvider>
518
+ <ReferencesPanel {...defaultProps} isAssisting={false} progress={null} />
564
519
  );
565
520
 
566
521
  // Selection UI should be immediately available (no button click needed)
@@ -594,13 +549,11 @@ describe('ReferencesPanel Component', () => {
594
549
 
595
550
  // Start detecting
596
551
  rerender(
597
- <EventBusProvider>
598
- <ReferencesPanel
599
- {...defaultProps}
600
- isAssisting={true}
601
- progress={{ completedEntityTypes: [] }}
602
- />
603
- </EventBusProvider>
552
+ <ReferencesPanel
553
+ {...defaultProps}
554
+ isAssisting={true}
555
+ progress={{ completedEntityTypes: [] }}
556
+ />
604
557
  );
605
558
 
606
559
  // Detecting state
@@ -622,22 +575,18 @@ describe('ReferencesPanel Component', () => {
622
575
 
623
576
  // Complete - first trigger useEffect to copy to lastDetectionLog
624
577
  rerender(
625
- <EventBusProvider>
626
- <ReferencesPanel
627
- {...defaultProps}
628
- isAssisting={false}
629
- progress={{
630
- completedEntityTypes: [{ entityType: 'Person', foundCount: 5 }],
631
- }}
632
- />
633
- </EventBusProvider>
578
+ <ReferencesPanel
579
+ {...defaultProps}
580
+ isAssisting={false}
581
+ progress={{
582
+ completedEntityTypes: [{ entityType: 'Person', foundCount: 5 }],
583
+ }}
584
+ />
634
585
  );
635
586
 
636
587
  // Then clear progress to show the log
637
588
  rerender(
638
- <EventBusProvider>
639
- <ReferencesPanel {...defaultProps} isAssisting={false} progress={null} />
640
- </EventBusProvider>
589
+ <ReferencesPanel {...defaultProps} isAssisting={false} progress={null} />
641
590
  );
642
591
 
643
592
  expect(screen.queryByTestId('annotation-progress-widget')).not.toBeInTheDocument();
@@ -659,18 +608,14 @@ describe('ReferencesPanel Component', () => {
659
608
 
660
609
  // Clear progress to show the log
661
610
  rerender(
662
- <EventBusProvider>
663
- <ReferencesPanel {...defaultProps} isAssisting={false} progress={null} />
664
- </EventBusProvider>
611
+ <ReferencesPanel {...defaultProps} isAssisting={false} progress={null} />
665
612
  );
666
613
 
667
614
  // Selection UI should be immediately available
668
615
  expect(screen.getByText('Select entity types')).toBeInTheDocument();
669
616
 
670
617
  rerender(
671
- <EventBusProvider>
672
- <ReferencesPanel {...defaultProps} />
673
- </EventBusProvider>
618
+ <ReferencesPanel {...defaultProps} />
674
619
  );
675
620
 
676
621
  expect(screen.getByText('Select entity types')).toBeInTheDocument();
@@ -2,8 +2,9 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
2
  import React from 'react';
3
3
  import { render, screen, fireEvent, waitFor } from '@testing-library/react';
4
4
  import '@testing-library/jest-dom';
5
+ import type { EventBus } from '@semiont/core';
5
6
  import { ResourceInfoPanel } from '../ResourceInfoPanel';
6
- import { EventBusProvider, useEventBus } from '../../../../contexts/EventBusContext';
7
+ import { createTestSemiontWrapper } from '../../../../test-utils';
7
8
 
8
9
  // Mock TranslationContext
9
10
  vi.mock('../../../../contexts/TranslationContext', () => ({
@@ -61,64 +62,31 @@ interface TrackedEvent {
61
62
 
62
63
  function createEventTracker() {
63
64
  const events: TrackedEvent[] = [];
64
-
65
- function EventTrackingWrapper({ children }: { children: React.ReactNode }) {
66
- const eventBus = useEventBus();
67
-
68
- React.useEffect(() => {
69
- const handlers: Array<() => void> = [];
70
-
71
- // Track resource-related events
72
- const trackEvent = (eventName: string) => (payload: any) => {
73
- events.push({ event: eventName, payload });
74
- };
75
-
65
+ return {
66
+ events,
67
+ clear: () => { events.length = 0; },
68
+ _attach(eventBus: EventBus) {
76
69
  const resourceEvents = [
77
70
  'yield:clone',
78
71
  'mark:archive',
79
72
  'mark:unarchive',
80
73
  ] as const;
81
-
82
- resourceEvents.forEach(eventName => {
83
- const handler = trackEvent(eventName);
84
- const subscription = eventBus.get(eventName).subscribe(handler);
85
- handlers.push(subscription);
74
+ resourceEvents.forEach((eventName) => {
75
+ eventBus.get(eventName).subscribe((payload: any) => {
76
+ events.push({ event: eventName, payload });
77
+ });
86
78
  });
87
-
88
- return () => {
89
- handlers.forEach(sub => sub.unsubscribe());
90
- };
91
- }, [eventBus]);
92
-
93
- return <>{children}</>;
94
- }
95
-
96
- return {
97
- EventTrackingWrapper,
98
- events,
99
- clear: () => {
100
- events.length = 0;
101
79
  },
102
80
  };
103
81
  }
104
82
 
105
- // Helper to render with EventBusProvider
106
83
  const renderWithEventBus = (component: React.ReactElement, tracker?: ReturnType<typeof createEventTracker>) => {
107
- if (tracker) {
108
- return render(
109
- <EventBusProvider>
110
- <tracker.EventTrackingWrapper>
111
- {component}
112
- </tracker.EventTrackingWrapper>
113
- </EventBusProvider>
114
- );
115
- }
116
-
117
- return render(
118
- <EventBusProvider>
119
- {component}
120
- </EventBusProvider>
84
+ const { SemiontWrapper, eventBus } = createTestSemiontWrapper();
85
+ if (tracker) tracker._attach(eventBus);
86
+ const Wrapper = ({ children }: { children: React.ReactNode }) => (
87
+ <SemiontWrapper>{children}</SemiontWrapper>
121
88
  );
89
+ return render(component, { wrapper: Wrapper });
122
90
  };
123
91
 
124
92
  describe('ResourceInfoPanel Component', () => {