@semiont/react-ui 0.2.33-build.79 → 0.2.33-build.80

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 (213) hide show
  1. package/dist/EventBusContext-7GvDyO0d.d.mts +414 -0
  2. package/dist/{PdfAnnotationCanvas.client-ADC4FFSE.mjs → PdfAnnotationCanvas.client-RAJRPQLU.mjs} +42 -27
  3. package/dist/PdfAnnotationCanvas.client-RAJRPQLU.mjs.map +1 -0
  4. package/dist/{ar-EMHEHPCJ.mjs → ar-4ZEORRW2.mjs} +7 -4
  5. package/dist/ar-4ZEORRW2.mjs.map +1 -0
  6. package/dist/{bn-OVCI4F6X.mjs → bn-SEDE5BQJ.mjs} +7 -4
  7. package/dist/bn-SEDE5BQJ.mjs.map +1 -0
  8. package/dist/{chunk-LIHZTECW.mjs → chunk-D7NBW4RV.mjs} +7 -4
  9. package/dist/chunk-D7NBW4RV.mjs.map +1 -0
  10. package/dist/{chunk-JZIO2A3B.mjs → chunk-ZR4ZV2LY.mjs} +206 -146
  11. package/dist/chunk-ZR4ZV2LY.mjs.map +1 -0
  12. package/dist/{cs-FAN66Q2F.mjs → cs-7W4WF5WD.mjs} +7 -4
  13. package/dist/cs-7W4WF5WD.mjs.map +1 -0
  14. package/dist/{da-YBBIHI2O.mjs → da-75XGBCBK.mjs} +7 -4
  15. package/dist/da-75XGBCBK.mjs.map +1 -0
  16. package/dist/{de-MAYU33LB.mjs → de-ODJVFLHM.mjs} +7 -4
  17. package/dist/de-ODJVFLHM.mjs.map +1 -0
  18. package/dist/{el-MKGSWN4O.mjs → el-C4PM4WB3.mjs} +7 -4
  19. package/dist/el-C4PM4WB3.mjs.map +1 -0
  20. package/dist/{en-DDLIXJCU.mjs → en-KJCJQ4OO.mjs} +2 -2
  21. package/dist/{es-52LHUWJD.mjs → es-WD33R7QL.mjs} +7 -4
  22. package/dist/es-WD33R7QL.mjs.map +1 -0
  23. package/dist/{fa-FJICRANB.mjs → fa-2BP6V56P.mjs} +7 -4
  24. package/dist/fa-2BP6V56P.mjs.map +1 -0
  25. package/dist/{fi-O455XFCR.mjs → fi-USRRW24J.mjs} +7 -4
  26. package/dist/fi-USRRW24J.mjs.map +1 -0
  27. package/dist/{fr-TXIXHOOE.mjs → fr-EC5S6WVF.mjs} +7 -4
  28. package/dist/fr-EC5S6WVF.mjs.map +1 -0
  29. package/dist/{he-JBSOX5IN.mjs → he-7TBVIKAA.mjs} +7 -4
  30. package/dist/he-7TBVIKAA.mjs.map +1 -0
  31. package/dist/{hi-KGHI3XVT.mjs → hi-FO4VIZLA.mjs} +7 -4
  32. package/dist/hi-FO4VIZLA.mjs.map +1 -0
  33. package/dist/{id-5OCPPZLO.mjs → id-7U7GGVWY.mjs} +7 -4
  34. package/dist/id-7U7GGVWY.mjs.map +1 -0
  35. package/dist/index.css +123 -85
  36. package/dist/index.css.map +1 -1
  37. package/dist/index.d.mts +645 -471
  38. package/dist/index.mjs +3461 -3025
  39. package/dist/index.mjs.map +1 -1
  40. package/dist/{it-PNBBZSM2.mjs → it-Y4OPL6I2.mjs} +7 -4
  41. package/dist/it-Y4OPL6I2.mjs.map +1 -0
  42. package/dist/{ja-LDD7R3TJ.mjs → ja-PK7SQL55.mjs} +7 -4
  43. package/dist/ja-PK7SQL55.mjs.map +1 -0
  44. package/dist/{ko-F47ZDEY3.mjs → ko-L25PXMYD.mjs} +7 -4
  45. package/dist/ko-L25PXMYD.mjs.map +1 -0
  46. package/dist/{ms-Z7LMXJWL.mjs → ms-STH777QM.mjs} +7 -4
  47. package/dist/ms-STH777QM.mjs.map +1 -0
  48. package/dist/{nl-6SJFBPJ3.mjs → nl-Y7LECDDR.mjs} +7 -4
  49. package/dist/nl-Y7LECDDR.mjs.map +1 -0
  50. package/dist/{no-YXPBPSGF.mjs → no-KEKCEWU6.mjs} +7 -4
  51. package/dist/no-KEKCEWU6.mjs.map +1 -0
  52. package/dist/{pl-P4AZ2QME.mjs → pl-7A7OC75O.mjs} +7 -4
  53. package/dist/pl-7A7OC75O.mjs.map +1 -0
  54. package/dist/{pt-LHWUS6U6.mjs → pt-35HTM7RA.mjs} +7 -4
  55. package/dist/pt-35HTM7RA.mjs.map +1 -0
  56. package/dist/{ro-EA5J2ZON.mjs → ro-VAWL5KQA.mjs} +7 -4
  57. package/dist/ro-VAWL5KQA.mjs.map +1 -0
  58. package/dist/{sv-DATBS3UQ.mjs → sv-7ZK5EQEB.mjs} +7 -4
  59. package/dist/sv-7ZK5EQEB.mjs.map +1 -0
  60. package/dist/test-utils.d.mts +18 -8
  61. package/dist/test-utils.mjs +36 -14
  62. package/dist/test-utils.mjs.map +1 -1
  63. package/dist/{th-WTFJRWPT.mjs → th-UDWZ4X34.mjs} +7 -4
  64. package/dist/th-UDWZ4X34.mjs.map +1 -0
  65. package/dist/{tr-IKO3RXOX.mjs → tr-4WMPK3UX.mjs} +7 -4
  66. package/dist/tr-4WMPK3UX.mjs.map +1 -0
  67. package/dist/{uk-CF6CTTRK.mjs → uk-SSLASQYJ.mjs} +7 -4
  68. package/dist/uk-SSLASQYJ.mjs.map +1 -0
  69. package/dist/{vi-AJLTXPZQ.mjs → vi-IF42Z5PU.mjs} +7 -4
  70. package/dist/vi-IF42Z5PU.mjs.map +1 -0
  71. package/dist/{zh-U3ORHHYH.mjs → zh-HRQTNTAI.mjs} +7 -4
  72. package/dist/zh-HRQTNTAI.mjs.map +1 -0
  73. package/package.json +3 -1
  74. package/src/components/CodeMirrorRenderer.tsx +66 -93
  75. package/src/components/DetectionProgressWidget.tsx +16 -5
  76. package/src/components/LiveRegion.tsx +18 -18
  77. package/src/components/ResizeHandle.tsx +10 -4
  78. package/src/components/SessionTimer.tsx +2 -2
  79. package/src/components/Toolbar.tsx +18 -9
  80. package/src/components/__tests__/SessionTimer.test.tsx +9 -9
  81. package/src/components/annotation/AnnotateToolbar.tsx +17 -15
  82. package/src/components/annotation/__tests__/AnnotateToolbar.test.tsx +165 -63
  83. package/src/components/annotation/annotation-entries.css +10 -0
  84. package/src/components/annotation-popups/JsonLdView.tsx +8 -2
  85. package/src/components/image-annotation/AnnotationOverlay.tsx +42 -22
  86. package/src/components/image-annotation/SvgDrawingCanvas.tsx +27 -30
  87. package/src/components/layout/__tests__/LeftSidebar.test.tsx +12 -33
  88. package/src/components/layout/__tests__/PageLayout.test.tsx +37 -32
  89. package/src/components/layout/__tests__/UnifiedHeader.test.tsx +21 -40
  90. package/src/components/modals/ResourceSearchModal.tsx +2 -2
  91. package/src/components/modals/SearchModal.tsx +1 -1
  92. package/src/components/navigation/CollapsibleResourceNavigation.tsx +14 -9
  93. package/src/components/navigation/NavigationTabs.css +36 -24
  94. package/src/components/navigation/ObservableLink.tsx +91 -0
  95. package/src/components/navigation/SimpleNavigation.tsx +20 -16
  96. package/src/components/navigation/SortableResourceTab.tsx +11 -5
  97. package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +51 -26
  98. package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +28 -22
  99. package/src/components/resource/AnnotateView.tsx +64 -134
  100. package/src/components/resource/BrowseView.tsx +86 -166
  101. package/src/components/resource/HistoryEvent.tsx +13 -7
  102. package/src/components/resource/ResourceViewer.tsx +122 -264
  103. package/src/components/resource/__tests__/BrowseView.test.tsx +631 -0
  104. package/src/components/resource/__tests__/ResourceViewer.mode-switch.test.tsx +231 -0
  105. package/src/components/resource/panels/AssessmentEntry.tsx +25 -33
  106. package/src/components/resource/panels/AssessmentPanel.tsx +106 -28
  107. package/src/components/resource/panels/CommentEntry.tsx +38 -32
  108. package/src/components/resource/panels/CommentsPanel.tsx +121 -28
  109. package/src/components/resource/panels/DetectSection.css +36 -1
  110. package/src/components/resource/panels/DetectSection.tsx +38 -10
  111. package/src/components/resource/panels/HighlightEntry.tsx +25 -33
  112. package/src/components/resource/panels/HighlightPanel.tsx +100 -25
  113. package/src/components/resource/panels/ReferenceEntry.tsx +61 -75
  114. package/src/components/resource/panels/ReferencesPanel.tsx +134 -42
  115. package/src/components/resource/panels/ResourceInfoPanel.tsx +47 -48
  116. package/src/components/resource/panels/TagEntry.tsx +25 -33
  117. package/src/components/resource/panels/TaggingPanel.tsx +119 -30
  118. package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +30 -92
  119. package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +129 -110
  120. package/src/components/resource/panels/__tests__/CommentEntry.test.tsx +86 -78
  121. package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +144 -149
  122. package/src/components/resource/panels/__tests__/DetectSection.test.tsx +480 -0
  123. package/src/components/resource/panels/__tests__/HighlightPanel.detectionProgress.test.tsx +362 -0
  124. package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +226 -111
  125. package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +117 -61
  126. package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +128 -106
  127. package/src/components/settings/SettingsPanel.tsx +15 -12
  128. package/src/features/admin-devops/__tests__/AdminDevOpsPage.test.tsx +1 -46
  129. package/src/features/admin-devops/components/AdminDevOpsPage.tsx +0 -9
  130. package/src/features/admin-security/__tests__/AdminSecurityPage.test.tsx +0 -3
  131. package/src/features/admin-security/components/AdminSecurityPage.tsx +0 -9
  132. package/src/features/admin-users/__tests__/AdminUsersPage.test.tsx +0 -3
  133. package/src/features/admin-users/components/AdminUsersPage.tsx +0 -9
  134. package/src/features/moderate-entity-tags/__tests__/EntityTagsPage.test.tsx +0 -3
  135. package/src/features/moderate-entity-tags/components/EntityTagsPage.tsx +1 -9
  136. package/src/features/moderate-recent/__tests__/RecentDocumentsPage.test.tsx +0 -32
  137. package/src/features/moderate-recent/components/RecentDocumentsPage.tsx +1 -9
  138. package/src/features/moderate-tag-schemas/__tests__/TagSchemasPage.test.tsx +0 -32
  139. package/src/features/moderate-tag-schemas/components/TagSchemasPage.tsx +1 -9
  140. package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +51 -54
  141. package/src/features/resource-compose/components/ResourceComposePage.tsx +3 -13
  142. package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +39 -45
  143. package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +9 -13
  144. package/src/features/resource-viewer/__tests__/AnnotationDeletionIntegration.test.tsx +231 -0
  145. package/src/features/resource-viewer/__tests__/DetectionFlowBug.test.tsx +234 -0
  146. package/src/features/resource-viewer/__tests__/DetectionFlowIntegration.test.tsx +388 -0
  147. package/src/features/resource-viewer/__tests__/DetectionProgressDismissal.test.tsx +318 -0
  148. package/src/features/resource-viewer/__tests__/GenerationFlowIntegration.test.tsx +504 -0
  149. package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +135 -88
  150. package/src/features/resource-viewer/__tests__/detection-progress-flow.test.tsx +322 -0
  151. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +308 -528
  152. package/translations/ar.json +6 -3
  153. package/translations/bn.json +6 -3
  154. package/translations/cs.json +6 -3
  155. package/translations/da.json +6 -3
  156. package/translations/de.json +6 -3
  157. package/translations/el.json +6 -3
  158. package/translations/en.json +6 -3
  159. package/translations/es.json +6 -3
  160. package/translations/fa.json +6 -3
  161. package/translations/fi.json +6 -3
  162. package/translations/fr.json +6 -3
  163. package/translations/he.json +6 -3
  164. package/translations/hi.json +6 -3
  165. package/translations/id.json +6 -3
  166. package/translations/it.json +6 -3
  167. package/translations/ja.json +6 -3
  168. package/translations/ko.json +6 -3
  169. package/translations/ms.json +6 -3
  170. package/translations/nl.json +6 -3
  171. package/translations/no.json +6 -3
  172. package/translations/pl.json +6 -3
  173. package/translations/pt.json +6 -3
  174. package/translations/ro.json +6 -3
  175. package/translations/sv.json +6 -3
  176. package/translations/th.json +6 -3
  177. package/translations/tr.json +6 -3
  178. package/translations/uk.json +6 -3
  179. package/translations/vi.json +6 -3
  180. package/translations/zh.json +6 -3
  181. package/dist/PdfAnnotationCanvas.client-ADC4FFSE.mjs.map +0 -1
  182. package/dist/TranslationManager-Co_5fSxl.d.mts +0 -118
  183. package/dist/ar-EMHEHPCJ.mjs.map +0 -1
  184. package/dist/bn-OVCI4F6X.mjs.map +0 -1
  185. package/dist/chunk-JZIO2A3B.mjs.map +0 -1
  186. package/dist/chunk-LIHZTECW.mjs.map +0 -1
  187. package/dist/cs-FAN66Q2F.mjs.map +0 -1
  188. package/dist/da-YBBIHI2O.mjs.map +0 -1
  189. package/dist/de-MAYU33LB.mjs.map +0 -1
  190. package/dist/el-MKGSWN4O.mjs.map +0 -1
  191. package/dist/es-52LHUWJD.mjs.map +0 -1
  192. package/dist/fa-FJICRANB.mjs.map +0 -1
  193. package/dist/fi-O455XFCR.mjs.map +0 -1
  194. package/dist/fr-TXIXHOOE.mjs.map +0 -1
  195. package/dist/he-JBSOX5IN.mjs.map +0 -1
  196. package/dist/hi-KGHI3XVT.mjs.map +0 -1
  197. package/dist/id-5OCPPZLO.mjs.map +0 -1
  198. package/dist/it-PNBBZSM2.mjs.map +0 -1
  199. package/dist/ja-LDD7R3TJ.mjs.map +0 -1
  200. package/dist/ko-F47ZDEY3.mjs.map +0 -1
  201. package/dist/ms-Z7LMXJWL.mjs.map +0 -1
  202. package/dist/nl-6SJFBPJ3.mjs.map +0 -1
  203. package/dist/no-YXPBPSGF.mjs.map +0 -1
  204. package/dist/pl-P4AZ2QME.mjs.map +0 -1
  205. package/dist/pt-LHWUS6U6.mjs.map +0 -1
  206. package/dist/ro-EA5J2ZON.mjs.map +0 -1
  207. package/dist/sv-DATBS3UQ.mjs.map +0 -1
  208. package/dist/th-WTFJRWPT.mjs.map +0 -1
  209. package/dist/tr-IKO3RXOX.mjs.map +0 -1
  210. package/dist/uk-CF6CTTRK.mjs.map +0 -1
  211. package/dist/vi-AJLTXPZQ.mjs.map +0 -1
  212. package/dist/zh-U3ORHHYH.mjs.map +0 -1
  213. /package/dist/{en-DDLIXJCU.mjs.map → en-KJCJQ4OO.mjs.map} +0 -0
@@ -1,22 +1,77 @@
1
1
  import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
2
  import type { MockedFunction } from 'vitest';
3
3
  import React from 'react';
4
- import { render, screen, fireEvent } from '@testing-library/react';
4
+ 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, resetEventBusForTesting, useEventBus } from '../../../../contexts/EventBusContext';
8
9
  import type { components } from '@semiont/api-client';
9
10
 
10
11
  type Annotation = components['schemas']['Annotation'];
11
12
 
12
- // Mock MakeMeaningEventBusContext
13
- vi.mock('../../../../contexts/MakeMeaningEventBusContext', () => ({
14
- useMakeMeaningEvents: vi.fn(() => ({
15
- emit: vi.fn(),
16
- on: vi.fn(),
17
- off: vi.fn(),
18
- })),
19
- }));
13
+ // Composition-based event tracker
14
+ interface TrackedEvent {
15
+ event: string;
16
+ payload: any;
17
+ }
18
+
19
+ function createEventTracker() {
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 = ['annotation:create'];
33
+
34
+ panelEvents.forEach(eventName => {
35
+ const handler = trackEvent(eventName);
36
+ eventBus.on(eventName, handler);
37
+ handlers.push(() => eventBus.off(eventName, handler));
38
+ });
39
+
40
+ return () => {
41
+ handlers.forEach(cleanup => cleanup());
42
+ };
43
+ }, [eventBus]);
44
+
45
+ return <>{children}</>;
46
+ }
47
+
48
+ return {
49
+ EventTrackingWrapper,
50
+ events,
51
+ clear: () => {
52
+ events.length = 0;
53
+ },
54
+ };
55
+ }
56
+
57
+ // Helper to render with EventBusProvider
58
+ 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>
73
+ );
74
+ };
20
75
 
21
76
  // Mock TranslationContext
22
77
  vi.mock('../../../../contexts/TranslationContext', () => ({
@@ -30,6 +85,7 @@ vi.mock('../../../../contexts/TranslationContext', () => ({
30
85
  };
31
86
  return translations[key] || key;
32
87
  }),
88
+ TranslationProvider: ({ children }: { children: React.ReactNode }) => children,
33
89
  }));
34
90
 
35
91
  // Mock @semiont/api-client utilities
@@ -118,13 +174,11 @@ const createPendingAnnotation = (exact: string) => ({
118
174
  describe('CommentsPanel Component', () => {
119
175
  const defaultProps = {
120
176
  annotations: mockComments.empty,
121
- onAnnotationClick: vi.fn(),
122
- onCreate: vi.fn(),
123
- focusedAnnotationId: null,
124
177
  pendingAnnotation: null,
125
178
  };
126
179
 
127
180
  beforeEach(() => {
181
+ resetEventBusForTesting();
128
182
  vi.clearAllMocks();
129
183
 
130
184
  // Mock scrollIntoView for jsdom
@@ -146,20 +200,21 @@ describe('CommentsPanel Component', () => {
146
200
 
147
201
  describe('Rendering', () => {
148
202
  it('should render panel header with title and count', () => {
149
- render(<CommentsPanel {...defaultProps} annotations={mockComments.multiple} />);
203
+ renderWithEventBus(<CommentsPanel {...defaultProps} annotations={mockComments.multiple} />);
150
204
 
151
- expect(screen.getByText(/Comments/)).toBeInTheDocument();
205
+ const headings = screen.getAllByText(/Comments/);
206
+ expect(headings.length).toBeGreaterThan(0);
152
207
  expect(screen.getByText(/\(3\)/)).toBeInTheDocument();
153
208
  });
154
209
 
155
210
  it('should show empty state when no comments', () => {
156
- render(<CommentsPanel {...defaultProps} />);
211
+ renderWithEventBus(<CommentsPanel {...defaultProps} />);
157
212
 
158
213
  expect(screen.getByText(/No comments yet/)).toBeInTheDocument();
159
214
  });
160
215
 
161
216
  it('should render all comments', () => {
162
- render(<CommentsPanel {...defaultProps} annotations={mockComments.multiple} />);
217
+ renderWithEventBus(<CommentsPanel {...defaultProps} annotations={mockComments.multiple} />);
163
218
 
164
219
  expect(screen.getByTestId('comment-1')).toBeInTheDocument();
165
220
  expect(screen.getByTestId('comment-2')).toBeInTheDocument();
@@ -167,7 +222,7 @@ describe('CommentsPanel Component', () => {
167
222
  });
168
223
 
169
224
  it('should have proper panel structure', () => {
170
- const { container } = render(<CommentsPanel {...defaultProps} />);
225
+ const { container } = renderWithEventBus(<CommentsPanel {...defaultProps} />);
171
226
 
172
227
  // Find the root panel div (first child of the container)
173
228
  const panel = container.firstChild as HTMLElement;
@@ -175,7 +230,7 @@ describe('CommentsPanel Component', () => {
175
230
  });
176
231
 
177
232
  it('should have scrollable comments list', () => {
178
- const { container } = render(
233
+ const { container } = renderWithEventBus(
179
234
  <CommentsPanel {...defaultProps} annotations={mockComments.many} />
180
235
  );
181
236
 
@@ -186,7 +241,7 @@ describe('CommentsPanel Component', () => {
186
241
 
187
242
  describe('Comment Sorting', () => {
188
243
  it('should sort comments by position in resource', () => {
189
- render(<CommentsPanel {...defaultProps} annotations={mockComments.multiple} />);
244
+ renderWithEventBus(<CommentsPanel {...defaultProps} annotations={mockComments.multiple} />);
190
245
 
191
246
  const comments = screen.getAllByTestId(/comment-/);
192
247
 
@@ -200,12 +255,12 @@ describe('CommentsPanel Component', () => {
200
255
  mockGetTextPositionSelector.mockReturnValue(null);
201
256
 
202
257
  expect(() => {
203
- render(<CommentsPanel {...defaultProps} annotations={mockComments.multiple} />);
258
+ renderWithEventBus(<CommentsPanel {...defaultProps} annotations={mockComments.multiple} />);
204
259
  }).not.toThrow();
205
260
  });
206
261
 
207
262
  it('should maintain sort order when comments update', () => {
208
- const { rerender } = render(
263
+ const { rerender } = renderWithEventBus(
209
264
  <CommentsPanel {...defaultProps} annotations={mockComments.multiple} />
210
265
  );
211
266
 
@@ -215,7 +270,11 @@ describe('CommentsPanel Component', () => {
215
270
  createMockComment('4', 25, 35),
216
271
  ];
217
272
 
218
- rerender(<CommentsPanel {...defaultProps} annotations={updatedComments} />);
273
+ rerender(
274
+ <EventBusProvider>
275
+ <CommentsPanel {...defaultProps} annotations={updatedComments} />
276
+ </EventBusProvider>
277
+ );
219
278
 
220
279
  const comments = screen.getAllByTestId(/comment-/);
221
280
 
@@ -229,7 +288,7 @@ describe('CommentsPanel Component', () => {
229
288
 
230
289
  describe('New Comment Creation', () => {
231
290
  it('should not show new comment input by default', () => {
232
- render(<CommentsPanel {...defaultProps} />);
291
+ renderWithEventBus(<CommentsPanel {...defaultProps} />);
233
292
 
234
293
  expect(screen.queryByPlaceholderText(/Add your comment/)).not.toBeInTheDocument();
235
294
  });
@@ -237,11 +296,10 @@ describe('CommentsPanel Component', () => {
237
296
  it('should show new comment input when pendingAnnotation exists', () => {
238
297
  const pendingAnnotation = createPendingAnnotation('Selected text');
239
298
 
240
- render(
299
+ renderWithEventBus(
241
300
  <CommentsPanel
242
301
  {...defaultProps}
243
302
  pendingAnnotation={pendingAnnotation}
244
- onCreate={vi.fn()}
245
303
  />
246
304
  );
247
305
 
@@ -251,11 +309,10 @@ describe('CommentsPanel Component', () => {
251
309
  it('should display quoted selected text in new comment area', () => {
252
310
  const pendingAnnotation = createPendingAnnotation('Selected text for comment');
253
311
 
254
- render(
312
+ renderWithEventBus(
255
313
  <CommentsPanel
256
314
  {...defaultProps}
257
315
  pendingAnnotation={pendingAnnotation}
258
- onCreate={vi.fn()}
259
316
  />
260
317
  );
261
318
 
@@ -266,11 +323,10 @@ describe('CommentsPanel Component', () => {
266
323
  const longText = 'A'.repeat(150);
267
324
  const pendingAnnotation = createPendingAnnotation(longText);
268
325
 
269
- render(
326
+ renderWithEventBus(
270
327
  <CommentsPanel
271
328
  {...defaultProps}
272
329
  pendingAnnotation={pendingAnnotation}
273
- onCreate={vi.fn()}
274
330
  />
275
331
  );
276
332
 
@@ -281,11 +337,10 @@ describe('CommentsPanel Component', () => {
281
337
  it('should allow typing in new comment textarea', async () => {
282
338
  const pendingAnnotation = createPendingAnnotation('Selected text');
283
339
 
284
- render(
340
+ renderWithEventBus(
285
341
  <CommentsPanel
286
342
  {...defaultProps}
287
343
  pendingAnnotation={pendingAnnotation}
288
- onCreate={vi.fn()}
289
344
  />
290
345
  );
291
346
 
@@ -298,11 +353,10 @@ describe('CommentsPanel Component', () => {
298
353
  it('should show character count', async () => {
299
354
  const pendingAnnotation = createPendingAnnotation('Selected text');
300
355
 
301
- render(
356
+ renderWithEventBus(
302
357
  <CommentsPanel
303
358
  {...defaultProps}
304
359
  pendingAnnotation={pendingAnnotation}
305
- onCreate={vi.fn()}
306
360
  />
307
361
  );
308
362
 
@@ -317,11 +371,10 @@ describe('CommentsPanel Component', () => {
317
371
  it('should enforce maxLength of 2000 characters', () => {
318
372
  const pendingAnnotation = createPendingAnnotation('Selected text');
319
373
 
320
- render(
374
+ renderWithEventBus(
321
375
  <CommentsPanel
322
376
  {...defaultProps}
323
377
  pendingAnnotation={pendingAnnotation}
324
- onCreate={vi.fn()}
325
378
  />
326
379
  );
327
380
 
@@ -332,11 +385,10 @@ describe('CommentsPanel Component', () => {
332
385
  it('should auto-focus new comment textarea', () => {
333
386
  const pendingAnnotation = createPendingAnnotation('Selected text');
334
387
 
335
- render(
388
+ renderWithEventBus(
336
389
  <CommentsPanel
337
390
  {...defaultProps}
338
391
  pendingAnnotation={pendingAnnotation}
339
- onCreate={vi.fn()}
340
392
  />
341
393
  );
342
394
 
@@ -344,16 +396,16 @@ describe('CommentsPanel Component', () => {
344
396
  expect(textarea).toHaveFocus();
345
397
  });
346
398
 
347
- it('should call onCreateComment when save is clicked', async () => {
348
- const onCreateComment = vi.fn();
399
+ it('should emit annotation:create event when save is clicked', async () => {
400
+ const tracker = createEventTracker();
349
401
  const pendingAnnotation = createPendingAnnotation('Selected text');
350
402
 
351
- render(
403
+ renderWithEventBus(
352
404
  <CommentsPanel
353
405
  {...defaultProps}
354
406
  pendingAnnotation={pendingAnnotation}
355
- onCreate={onCreateComment}
356
- />
407
+ />,
408
+ tracker
357
409
  );
358
410
 
359
411
  const textarea = screen.getByPlaceholderText(/Add your comment/);
@@ -362,17 +414,22 @@ describe('CommentsPanel Component', () => {
362
414
  const saveButton = screen.getByText('Save');
363
415
  await userEvent.click(saveButton);
364
416
 
365
- expect(onCreateComment).toHaveBeenCalledWith('My new comment');
417
+ await waitFor(() => {
418
+ expect(tracker.events.some(e =>
419
+ e.event === 'annotation:create' &&
420
+ e.payload?.motivation === 'commenting' &&
421
+ e.payload?.body?.[0]?.value === 'My new comment'
422
+ )).toBe(true);
423
+ });
366
424
  });
367
425
 
368
- it('should clear textarea after successful save', async () => {
426
+ it('should clear textarea after save is clicked', async () => {
369
427
  const pendingAnnotation = createPendingAnnotation('Selected text');
370
428
 
371
- render(
429
+ renderWithEventBus(
372
430
  <CommentsPanel
373
431
  {...defaultProps}
374
432
  pendingAnnotation={pendingAnnotation}
375
- onCreate={vi.fn()}
376
433
  />
377
434
  );
378
435
 
@@ -386,11 +443,10 @@ describe('CommentsPanel Component', () => {
386
443
  it('should disable save button when textarea is empty', () => {
387
444
  const pendingAnnotation = createPendingAnnotation('Selected text');
388
445
 
389
- render(
446
+ renderWithEventBus(
390
447
  <CommentsPanel
391
448
  {...defaultProps}
392
449
  pendingAnnotation={pendingAnnotation}
393
- onCreate={vi.fn()}
394
450
  />
395
451
  );
396
452
 
@@ -401,11 +457,10 @@ describe('CommentsPanel Component', () => {
401
457
  it('should disable save button when textarea contains only whitespace', async () => {
402
458
  const pendingAnnotation = createPendingAnnotation('Selected text');
403
459
 
404
- render(
460
+ renderWithEventBus(
405
461
  <CommentsPanel
406
462
  {...defaultProps}
407
463
  pendingAnnotation={pendingAnnotation}
408
- onCreate={vi.fn()}
409
464
  />
410
465
  );
411
466
 
@@ -419,11 +474,10 @@ describe('CommentsPanel Component', () => {
419
474
  it('should enable save button when text is entered', async () => {
420
475
  const pendingAnnotation = createPendingAnnotation('Selected text');
421
476
 
422
- render(
477
+ renderWithEventBus(
423
478
  <CommentsPanel
424
479
  {...defaultProps}
425
480
  pendingAnnotation={pendingAnnotation}
426
- onCreate={vi.fn()}
427
481
  />
428
482
  );
429
483
 
@@ -437,11 +491,10 @@ describe('CommentsPanel Component', () => {
437
491
  it('should have proper styling for new comment area', () => {
438
492
  const pendingAnnotation = createPendingAnnotation('Selected text');
439
493
 
440
- const { container } = render(
494
+ const { container } = renderWithEventBus(
441
495
  <CommentsPanel
442
496
  {...defaultProps}
443
497
  pendingAnnotation={pendingAnnotation}
444
- onCreate={vi.fn()}
445
498
  />
446
499
  );
447
500
 
@@ -452,76 +505,24 @@ describe('CommentsPanel Component', () => {
452
505
  });
453
506
 
454
507
  describe('Comment Interactions', () => {
455
- it('should call onCommentClick when comment is clicked', () => {
456
- const onCommentClick = vi.fn();
457
- render(
508
+ it('should render comment entries', () => {
509
+ renderWithEventBus(
458
510
  <CommentsPanel
459
511
  {...defaultProps}
460
512
  annotations={mockComments.single}
461
- onAnnotationClick={onCommentClick}
462
513
  />
463
514
  );
464
515
 
465
516
  const comment = screen.getByTestId('comment-1');
466
- fireEvent.click(comment);
467
-
468
- expect(onCommentClick).toHaveBeenCalledWith(mockComments.single[0]);
517
+ expect(comment).toBeInTheDocument();
469
518
  });
470
519
 
471
520
  });
472
521
 
473
522
  describe('Comment Hover Behavior', () => {
474
- it('should call onAnnotationHover when provided', () => {
475
- const onAnnotationHover = vi.fn();
476
- render(
477
- <CommentsPanel
478
- {...defaultProps}
479
- annotations={mockComments.single}
480
- onAnnotationHover={onAnnotationHover}
481
- />
482
- );
483
-
484
- const hoverButton = screen.getByText('Hover');
485
- fireEvent.mouseEnter(hoverButton);
486
-
487
- expect(onAnnotationHover).toHaveBeenCalledWith('1');
488
- });
489
-
490
- it('should handle hoveredAnnotationId prop changes', () => {
491
- const { rerender } = render(
492
- <CommentsPanel
493
- {...defaultProps}
494
- annotations={mockComments.multiple}
495
- hoveredAnnotationId={null}
496
- />
497
- );
498
-
499
- // Should not error when hoveredAnnotationId changes
500
- expect(() => {
501
- rerender(
502
- <CommentsPanel
503
- {...defaultProps}
504
- annotations={mockComments.multiple}
505
- hoveredAnnotationId="2"
506
- />
507
- );
508
- }).not.toThrow();
509
-
510
- // Should handle being set back to null
523
+ it('should render without errors', () => {
511
524
  expect(() => {
512
- rerender(
513
- <CommentsPanel
514
- {...defaultProps}
515
- annotations={mockComments.multiple}
516
- hoveredAnnotationId={null}
517
- />
518
- );
519
- }).not.toThrow();
520
- });
521
-
522
- it('should not error when onAnnotationHover is not provided', () => {
523
- expect(() => {
524
- render(
525
+ renderWithEventBus(
525
526
  <CommentsPanel
526
527
  {...defaultProps}
527
528
  annotations={mockComments.single}
@@ -532,51 +533,39 @@ describe('CommentsPanel Component', () => {
532
533
  });
533
534
 
534
535
  describe('Focus Management', () => {
535
- it('should pass focusedAnnotationId to CommentEntry components', () => {
536
- render(
536
+ it('should render comments', () => {
537
+ renderWithEventBus(
537
538
  <CommentsPanel
538
539
  {...defaultProps}
539
540
  annotations={mockComments.multiple}
540
- focusedAnnotationId="2"
541
541
  />
542
542
  );
543
543
 
544
- // The focused comment should be rendered
544
+ // Comments should be rendered
545
545
  expect(screen.getByTestId('comment-2')).toBeInTheDocument();
546
546
  });
547
-
548
- it('should handle null focusedAnnotationId', () => {
549
- expect(() => {
550
- render(
551
- <CommentsPanel
552
- {...defaultProps}
553
- annotations={mockComments.multiple}
554
- focusedAnnotationId={null}
555
- />
556
- );
557
- }).not.toThrow();
558
- });
559
547
  });
560
548
 
561
549
  describe('Panel Structure and Styling', () => {
562
550
  it('should have fixed header that does not scroll', () => {
563
- render(
551
+ renderWithEventBus(
564
552
  <CommentsPanel {...defaultProps} annotations={mockComments.many} />
565
553
  );
566
554
 
567
- const header = screen.getByText(/Comments/).closest('div');
555
+ const headers = screen.getAllByText(/Comments/);
556
+ const header = headers[0].closest('div');
568
557
  expect(header).toHaveClass('semiont-panel-header');
569
558
  });
570
559
 
571
560
  it('should support dark mode', () => {
572
- const { container } = render(<CommentsPanel {...defaultProps} />);
561
+ const { container } = renderWithEventBus(<CommentsPanel {...defaultProps} />);
573
562
 
574
563
  const panel = container.firstChild as HTMLElement;
575
564
  expect(panel).toHaveClass('semiont-panel');
576
565
  });
577
566
 
578
567
  it('should have proper spacing between comments', () => {
579
- const { container } = render(
568
+ const { container } = renderWithEventBus(
580
569
  <CommentsPanel {...defaultProps} annotations={mockComments.multiple} />
581
570
  );
582
571
 
@@ -585,9 +574,10 @@ describe('CommentsPanel Component', () => {
585
574
  });
586
575
 
587
576
  it('should have proper border styling', () => {
588
- render(<CommentsPanel {...defaultProps} />);
577
+ renderWithEventBus(<CommentsPanel {...defaultProps} />);
589
578
 
590
- const header = screen.getByText(/Comments/).closest('div');
579
+ const headers = screen.getAllByText(/Comments/);
580
+ const header = headers[0].closest('div');
591
581
  expect(header).toHaveClass('semiont-panel-header');
592
582
  });
593
583
  });
@@ -595,14 +585,14 @@ describe('CommentsPanel Component', () => {
595
585
  describe('Edge Cases', () => {
596
586
  it('should handle empty comments array', () => {
597
587
  expect(() => {
598
- render(<CommentsPanel {...defaultProps} annotations={[]} />);
588
+ renderWithEventBus(<CommentsPanel {...defaultProps} annotations={[]} />);
599
589
  }).not.toThrow();
600
590
 
601
591
  expect(screen.getByText(/No comments yet/)).toBeInTheDocument();
602
592
  });
603
593
 
604
594
  it('should handle rapid comment additions', () => {
605
- const { rerender } = render(
595
+ const { rerender } = renderWithEventBus(
606
596
  <CommentsPanel {...defaultProps} annotations={mockComments.empty} />
607
597
  );
608
598
 
@@ -610,37 +600,43 @@ describe('CommentsPanel Component', () => {
610
600
  const comments = Array.from({ length: i }, (_, j) =>
611
601
  createMockComment(`${j + 1}`, j * 10, (j + 1) * 10)
612
602
  );
613
- rerender(<CommentsPanel {...defaultProps} annotations={comments} />);
603
+ rerender(
604
+ <EventBusProvider>
605
+ <CommentsPanel {...defaultProps} annotations={comments} />
606
+ </EventBusProvider>
607
+ );
614
608
  }
615
609
 
616
610
  expect(screen.getAllByTestId(/comment-/)).toHaveLength(5);
617
611
  });
618
612
 
619
613
  it('should handle comment removal', () => {
620
- const { rerender } = render(
614
+ const { rerender } = renderWithEventBus(
621
615
  <CommentsPanel {...defaultProps} annotations={mockComments.multiple} />
622
616
  );
623
617
 
624
618
  expect(screen.getAllByTestId(/comment-/)).toHaveLength(3);
625
619
 
626
620
  rerender(
627
- <CommentsPanel {...defaultProps} annotations={mockComments.single} />
621
+ <EventBusProvider>
622
+ <CommentsPanel {...defaultProps} annotations={mockComments.single} />
623
+ </EventBusProvider>
628
624
  );
629
625
 
630
626
  expect(screen.getAllByTestId(/comment-/)).toHaveLength(1);
631
627
  });
632
628
 
633
- it('should show new comment input even without onCreate callback', () => {
629
+ it('should show new comment input when pendingAnnotation exists', () => {
634
630
  const pendingAnnotation = createPendingAnnotation('Selected text');
635
631
 
636
- render(
632
+ renderWithEventBus(
637
633
  <CommentsPanel
638
634
  {...defaultProps}
639
635
  pendingAnnotation={pendingAnnotation}
640
636
  />
641
637
  );
642
638
 
643
- // Component shows textarea when pendingAnnotation exists, even if onCreate is from defaultProps
639
+ // Component shows textarea when pendingAnnotation exists
644
640
  expect(screen.getByPlaceholderText(/Add your comment/)).toBeInTheDocument();
645
641
  });
646
642
 
@@ -650,7 +646,7 @@ describe('CommentsPanel Component', () => {
650
646
  );
651
647
 
652
648
  expect(() => {
653
- render(<CommentsPanel {...defaultProps} annotations={manyComments} />);
649
+ renderWithEventBus(<CommentsPanel {...defaultProps} annotations={manyComments} />);
654
650
  }).not.toThrow();
655
651
 
656
652
  expect(screen.getAllByTestId(/comment-/)).toHaveLength(100);
@@ -664,7 +660,7 @@ describe('CommentsPanel Component', () => {
664
660
  ];
665
661
 
666
662
  expect(() => {
667
- render(<CommentsPanel {...defaultProps} annotations={commentsAtSamePosition} />);
663
+ renderWithEventBus(<CommentsPanel {...defaultProps} annotations={commentsAtSamePosition} />);
668
664
  }).not.toThrow();
669
665
 
670
666
  expect(screen.getAllByTestId(/comment-/)).toHaveLength(3);
@@ -673,10 +669,10 @@ describe('CommentsPanel Component', () => {
673
669
 
674
670
  describe('Accessibility', () => {
675
671
  it('should have proper heading structure', () => {
676
- render(<CommentsPanel {...defaultProps} />);
672
+ renderWithEventBus(<CommentsPanel {...defaultProps} />);
677
673
 
678
- const heading = screen.getByText(/Comments/);
679
- expect(heading).toHaveClass('semiont-panel-header__text');
674
+ const headings = screen.getAllByText(/Comments/);
675
+ expect(headings[0]).toHaveClass('semiont-panel-header__text');
680
676
  });
681
677
 
682
678
  it('should have proper textarea attributes for new comments', () => {
@@ -688,11 +684,10 @@ describe('CommentsPanel Component', () => {
688
684
  },
689
685
  };
690
686
 
691
- render(
687
+ renderWithEventBus(
692
688
  <CommentsPanel
693
689
  {...defaultProps}
694
690
  pendingAnnotation={pendingAnnotation}
695
- onCreate={vi.fn()}
696
691
  />
697
692
  );
698
693
 
@@ -701,7 +696,7 @@ describe('CommentsPanel Component', () => {
701
696
  });
702
697
 
703
698
  it('should have semantic HTML structure', () => {
704
- const { container } = render(
699
+ const { container } = renderWithEventBus(
705
700
  <CommentsPanel {...defaultProps} annotations={mockComments.multiple} />
706
701
  );
707
702