@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
@@ -0,0 +1,234 @@
1
+ /**
2
+ * FAILING TEST: Reproduces the bug where detection events fire but state doesn't update
3
+ *
4
+ * Based on console logs from production:
5
+ * ✅ detection:start emitted
6
+ * ✅ detection:progress emitted
7
+ * ❌ detectingMotivation remains null
8
+ * ❌ detectionProgress remains null
9
+ *
10
+ * UPDATED: Now tests useDetectionFlow hook instead of DetectionFlowContainer
11
+ */
12
+
13
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
14
+ import { render, screen, waitFor } from '@testing-library/react';
15
+ import { act } from 'react';
16
+ import { useDetectionFlow } from '../../../hooks/useDetectionFlow';
17
+ import { EventBusProvider, useEventBus, resetEventBusForTesting } from '../../../contexts/EventBusContext';
18
+ import { ApiClientProvider } from '../../../contexts/ApiClientContext';
19
+ import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
20
+ import { SSEClient } from '@semiont/api-client';
21
+
22
+ describe('REPRODUCING BUG: Detection state not updating', () => {
23
+ beforeEach(() => {
24
+ resetEventBusForTesting();
25
+ vi.clearAllMocks();
26
+
27
+ // Minimal mock - SSE streams not needed for this test
28
+ vi.spyOn(SSEClient.prototype, 'detectAnnotations').mockReturnValue({ onProgress: vi.fn().mockReturnThis(), onComplete: vi.fn().mockReturnThis(), onError: vi.fn().mockReturnThis(), close: vi.fn() } as any);
29
+ vi.spyOn(SSEClient.prototype, 'detectHighlights').mockReturnValue({ onProgress: vi.fn().mockReturnThis(), onComplete: vi.fn().mockReturnThis(), onError: vi.fn().mockReturnThis(), close: vi.fn() } as any);
30
+ vi.spyOn(SSEClient.prototype, 'detectComments').mockReturnValue({ onProgress: vi.fn().mockReturnThis(), onComplete: vi.fn().mockReturnThis(), onError: vi.fn().mockReturnThis(), close: vi.fn() } as any);
31
+ vi.spyOn(SSEClient.prototype, 'detectAssessments').mockReturnValue({ onProgress: vi.fn().mockReturnThis(), onComplete: vi.fn().mockReturnThis(), onError: vi.fn().mockReturnThis(), close: vi.fn() } as any);
32
+ });
33
+
34
+ afterEach(() => {
35
+ vi.restoreAllMocks();
36
+ });
37
+
38
+ it('SHOULD update state when detection:start event is emitted', async () => {
39
+ let eventBusInstance: any;
40
+ let currentState: any;
41
+
42
+ // Component to capture EventBus and hook state
43
+ function TestComponent() {
44
+ eventBusInstance = useEventBus();
45
+ const state = useDetectionFlow('http://localhost:8080/resources/test' as any);
46
+ currentState = state;
47
+
48
+ console.log('[TEST] useDetectionFlow state:', {
49
+ detectingMotivation: state.detectingMotivation,
50
+ detectionProgress: state.detectionProgress,
51
+ });
52
+
53
+ return (
54
+ <div>
55
+ <div data-testid="detecting">{state.detectingMotivation || 'null'}</div>
56
+ <div data-testid="progress">{state.detectionProgress?.message || 'null'}</div>
57
+ </div>
58
+ );
59
+ }
60
+
61
+ render(
62
+ <EventBusProvider>
63
+ <AuthTokenProvider token={null}>
64
+ <ApiClientProvider baseUrl="http://localhost:4000">
65
+ <TestComponent />
66
+ </ApiClientProvider>
67
+ </AuthTokenProvider>
68
+ </EventBusProvider>
69
+ );
70
+
71
+ // Initial state should be null
72
+ expect(screen.getByTestId('detecting')).toHaveTextContent('null');
73
+ expect(screen.getByTestId('progress')).toHaveTextContent('null');
74
+
75
+ console.log('[TEST] Emitting detection:start event...');
76
+
77
+ // Emit detection:start event (exactly like production)
78
+ act(() => {
79
+ eventBusInstance.emit('detection:start', {
80
+ motivation: 'linking',
81
+ options: { entityTypes: ['Location'] }
82
+ });
83
+ });
84
+
85
+ console.log('[TEST] After detection:start, checking state...');
86
+
87
+ // THIS SHOULD PASS but currently FAILS
88
+ await waitFor(() => {
89
+ expect(screen.getByTestId('detecting')).toHaveTextContent('linking');
90
+ }, { timeout: 1000 });
91
+
92
+ expect(currentState.detectingMotivation).toBe('linking');
93
+ expect(currentState.detectionProgress).toBeNull(); // Should clear on start
94
+ });
95
+
96
+ it('SHOULD update state when detection:progress event is emitted', async () => {
97
+ let eventBusInstance: any;
98
+ let currentState: any;
99
+
100
+ function TestComponent() {
101
+ eventBusInstance = useEventBus();
102
+ const state = useDetectionFlow('http://localhost:8080/resources/test' as any);
103
+ currentState = state;
104
+
105
+ return (
106
+ <div>
107
+ <div data-testid="detecting">{state.detectingMotivation || 'null'}</div>
108
+ <div data-testid="progress">{state.detectionProgress?.message || 'null'}</div>
109
+ </div>
110
+ );
111
+ }
112
+
113
+ render(
114
+ <EventBusProvider>
115
+ <AuthTokenProvider token={null}>
116
+ <ApiClientProvider baseUrl="http://localhost:4000">
117
+ <TestComponent />
118
+ </ApiClientProvider>
119
+ </AuthTokenProvider>
120
+ </EventBusProvider>
121
+ );
122
+
123
+ console.log('[TEST] Emitting detection:progress event...');
124
+
125
+ // Emit detection:progress event (exactly like production)
126
+ act(() => {
127
+ eventBusInstance.emit('detection:progress', {
128
+ status: 'started',
129
+ resourceId: 'test',
130
+ totalEntityTypes: 1,
131
+ processedEntityTypes: 0,
132
+ message: 'Starting entity detection...'
133
+ });
134
+ });
135
+
136
+ console.log('[TEST] After detection:progress, checking state...');
137
+
138
+ // THIS SHOULD PASS but currently FAILS
139
+ await waitFor(() => {
140
+ expect(screen.getByTestId('progress')).toHaveTextContent('Starting entity detection...');
141
+ }, { timeout: 1000 });
142
+
143
+ expect(currentState.detectionProgress).toMatchObject({
144
+ status: 'started',
145
+ message: 'Starting entity detection...'
146
+ });
147
+ });
148
+
149
+ it('SHOULD show EXACTLY the production bug', async () => {
150
+ let eventBusInstance: any;
151
+ const stateSnapshots: any[] = [];
152
+
153
+ function TestComponent() {
154
+ eventBusInstance = useEventBus();
155
+ const state = useDetectionFlow('http://localhost:8080/resources/f45fd44f9cb0b0fe1b7980d3d034bc61' as any);
156
+
157
+ stateSnapshots.push({
158
+ detectingMotivation: state.detectingMotivation,
159
+ detectionProgress: state.detectionProgress,
160
+ });
161
+
162
+ return (
163
+ <div>
164
+ <div data-testid="detecting">{state.detectingMotivation || 'null'}</div>
165
+ <div data-testid="progress">{state.detectionProgress?.message || 'null'}</div>
166
+ </div>
167
+ );
168
+ }
169
+
170
+ render(
171
+ <EventBusProvider>
172
+ <AuthTokenProvider token={null}>
173
+ <ApiClientProvider baseUrl="http://localhost:4000">
174
+ <TestComponent />
175
+ </ApiClientProvider>
176
+ </AuthTokenProvider>
177
+ </EventBusProvider>
178
+ );
179
+
180
+ console.log('\n=== REPRODUCING PRODUCTION BUG ===');
181
+ console.log('Initial state:', stateSnapshots[stateSnapshots.length - 1]);
182
+
183
+ // Exactly like production logs
184
+ act(() => {
185
+ console.log('[EventBus] emit: detection:start {motivation: "linking", options: {...}}');
186
+ eventBusInstance.emit('detection:start', {
187
+ motivation: 'linking',
188
+ options: { entityTypes: ['Location'] }
189
+ });
190
+ });
191
+
192
+ console.log('After detection:start:', stateSnapshots[stateSnapshots.length - 1]);
193
+
194
+ act(() => {
195
+ console.log('[EventBus] emit: detection:progress {status: "started", ...}');
196
+ eventBusInstance.emit('detection:progress', {
197
+ status: 'started',
198
+ resourceId: 'f45fd44f9cb0b0fe1b7980d3d034bc61',
199
+ totalEntityTypes: 1,
200
+ processedEntityTypes: 0,
201
+ message: 'Starting entity detection...'
202
+ });
203
+ });
204
+
205
+ console.log('After detection:progress:', stateSnapshots[stateSnapshots.length - 1]);
206
+
207
+ act(() => {
208
+ console.log('[EventBus] emit: detection:progress {status: "scanning", ...}');
209
+ eventBusInstance.emit('detection:progress', {
210
+ status: 'scanning',
211
+ resourceId: 'f45fd44f9cb0b0fe1b7980d3d034bc61',
212
+ currentEntityType: 'Location',
213
+ totalEntityTypes: 1,
214
+ processedEntityTypes: 1,
215
+ message: 'Scanning for Location...'
216
+ });
217
+ });
218
+
219
+ console.log('After second detection:progress:', stateSnapshots[stateSnapshots.length - 1]);
220
+ console.log('=== END REPRODUCTION ===\n');
221
+
222
+ // THIS IS THE BUG: Events fire but state never updates
223
+ // Production logs show: detectingMotivation: null, detectionProgress: null
224
+ // Even though events were emitted
225
+ await waitFor(() => {
226
+ const currentSnapshot = stateSnapshots[stateSnapshots.length - 1];
227
+ console.log('Final state check:', currentSnapshot);
228
+
229
+ // These SHOULD pass but will FAIL if bug is present
230
+ expect(currentSnapshot.detectingMotivation).toBe('linking');
231
+ expect(currentSnapshot.detectionProgress?.message).toBe('Scanning for Location...');
232
+ }, { timeout: 2000 });
233
+ });
234
+ });
@@ -0,0 +1,388 @@
1
+ /**
2
+ * Layer 3: Feature Integration Test - Detection Flow Architecture
3
+ *
4
+ * Tests the COMPLETE detection flow with real component composition:
5
+ * - EventBusProvider (REAL)
6
+ * - ApiClientProvider (REAL, with MOCKED client)
7
+ * - useDetectionFlow (REAL)
8
+ * - useEventOperations (REAL)
9
+ * - useEventSubscriptions (REAL)
10
+ *
11
+ * This test focuses on ARCHITECTURE and EVENT WIRING:
12
+ * - Verifies API called exactly ONCE (catches duplicate subscriptions)
13
+ * - Tests event propagation through the event bus
14
+ * - Validates different motivations call correct API methods
15
+ * - Ensures multiple event listeners don't cause duplicate API calls
16
+ *
17
+ * COMPLEMENTARY TEST: See detection-progress-flow.test.tsx for UI/UX testing
18
+ * - That test verifies the USER EXPERIENCE (button clicks, progress display)
19
+ * - This test verifies the SYSTEM ARCHITECTURE (event wiring, API calls)
20
+ *
21
+ * NO BACKEND SERVER - only mocked API client boundary
22
+ */
23
+
24
+ import React from 'react';
25
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
26
+ import { render, screen, waitFor } from '@testing-library/react';
27
+ import { act } from 'react';
28
+ import { useDetectionFlow } from '../../../hooks/useDetectionFlow';
29
+ import { EventBusProvider, useEventBus, resetEventBusForTesting } from '../../../contexts/EventBusContext';
30
+ import { ApiClientProvider } from '../../../contexts/ApiClientContext';
31
+ import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
32
+ import { SSEClient } from '@semiont/api-client';
33
+ import type { Motivation } from '@semiont/api-client';
34
+ import { resourceUri } from '@semiont/api-client';
35
+ import type { Emitter } from 'mitt';
36
+ import type { EventMap } from '../../../contexts/EventBusContext';
37
+
38
+ // Mock SSE stream that we can control in tests
39
+ const createMockSSEStream = () => {
40
+ const stream = {
41
+ onProgressCallback: null as ((chunk: any) => void) | null,
42
+ onCompleteCallback: null as (() => void) | null,
43
+ onErrorCallback: null as ((error: Error) => void) | null,
44
+ onProgress: vi.fn((callback: (chunk: any) => void) => {
45
+ stream.onProgressCallback = callback;
46
+ return stream;
47
+ }),
48
+ onComplete: vi.fn((callback: () => void) => {
49
+ stream.onCompleteCallback = callback;
50
+ return stream;
51
+ }),
52
+ onError: vi.fn((callback: (error: Error) => void) => {
53
+ stream.onErrorCallback = callback;
54
+ return stream;
55
+ }),
56
+ close: vi.fn(),
57
+ };
58
+ return stream;
59
+ };
60
+
61
+ describe('Detection Flow - Feature Integration', () => {
62
+ let mockStream: ReturnType<typeof createMockSSEStream>;
63
+ let detectAnnotationsSpy: any;
64
+ let detectHighlightsSpy: any;
65
+ let detectCommentsSpy: any;
66
+
67
+ beforeEach(() => {
68
+ vi.clearAllMocks();
69
+ resetEventBusForTesting();
70
+
71
+ // Create fresh mock stream for each test
72
+ mockStream = createMockSSEStream();
73
+
74
+ // Spy on SSEClient prototype methods
75
+ detectAnnotationsSpy = vi.spyOn(SSEClient.prototype, 'detectAnnotations').mockReturnValue(mockStream as any);
76
+ detectHighlightsSpy = vi.spyOn(SSEClient.prototype, 'detectHighlights').mockReturnValue(mockStream as any);
77
+ detectCommentsSpy = vi.spyOn(SSEClient.prototype, 'detectComments').mockReturnValue(mockStream as any);
78
+ vi.spyOn(SSEClient.prototype, 'detectAssessments').mockReturnValue(mockStream as any);
79
+ });
80
+
81
+ afterEach(() => {
82
+ vi.restoreAllMocks();
83
+ });
84
+
85
+ it('should call detectAnnotations exactly ONCE when detection starts (not twice)', async () => {
86
+ const testUri = resourceUri('http://localhost:4000/resources/test-resource');
87
+
88
+ // Render with real component composition
89
+ const { emitDetectionStart } = renderDetectionFlow(testUri);
90
+
91
+ // Trigger detection for linking (uses detectAnnotations)
92
+ act(() => {
93
+ emitDetectionStart('linking', {
94
+ entityTypes: ['Person', 'Organization'],
95
+ includeDescriptiveReferences: false
96
+ });
97
+ });
98
+
99
+ // CRITICAL ASSERTION: API called exactly once (not twice!)
100
+ // This would FAIL if useEventOperations was called in multiple places
101
+ await waitFor(() => {
102
+ expect(detectAnnotationsSpy).toHaveBeenCalledTimes(1);
103
+ });
104
+
105
+ // Verify correct parameters
106
+ expect(detectAnnotationsSpy).toHaveBeenCalledWith(
107
+ testUri,
108
+ {
109
+ entityTypes: ['Person', 'Organization'],
110
+ includeDescriptiveReferences: false,
111
+ },
112
+ { auth: undefined }
113
+ );
114
+ });
115
+
116
+ it('should propagate SSE progress events to useDetectionFlow state', async () => {
117
+ const testUri = resourceUri('http://localhost:4000/resources/test-resource');
118
+
119
+ // Render with state observer
120
+ const { emitDetectionStart } = renderDetectionFlow(testUri);
121
+
122
+ // Start detection
123
+ act(() => {
124
+ emitDetectionStart('linking', {
125
+ entityTypes: ['Person']
126
+ });
127
+ });
128
+
129
+ // Wait for stream to be created
130
+ await waitFor(() => {
131
+ expect(detectAnnotationsSpy).toHaveBeenCalled();
132
+ });
133
+
134
+ // Simulate SSE progress callback being invoked
135
+ act(() => {
136
+ mockStream.onProgressCallback!({
137
+ status: 'scanning',
138
+ message: 'Scanning for Person...',
139
+ currentEntityType: 'Person',
140
+ totalEntityTypes: 1,
141
+ processedEntityTypes: 0,
142
+ foundCount: 5,
143
+ });
144
+ });
145
+
146
+ // Verify progress propagated to UI
147
+ await waitFor(() => {
148
+ expect(screen.getByTestId('progress')).toHaveTextContent('Scanning for Person...');
149
+ expect(screen.getByTestId('detecting')).toHaveTextContent('linking');
150
+ });
151
+ });
152
+
153
+ it('should handle multiple progress updates correctly', async () => {
154
+ const testUri = resourceUri('http://localhost:4000/resources/test-resource');
155
+ const { emitDetectionStart } = renderDetectionFlow(testUri);
156
+
157
+ // Start detection
158
+ act(() => {
159
+ emitDetectionStart('highlighting', {
160
+ instructions: 'Find important passages'
161
+ });
162
+ });
163
+
164
+ await waitFor(() => {
165
+ expect(detectHighlightsSpy).toHaveBeenCalledTimes(1);
166
+ });
167
+
168
+ // First progress update
169
+ act(() => {
170
+ mockStream.onProgressCallback!({
171
+ status: 'started',
172
+ message: 'Starting analysis...',
173
+ percentage: 0,
174
+ });
175
+ });
176
+
177
+ await waitFor(() => {
178
+ expect(screen.getByTestId('progress')).toHaveTextContent('Starting analysis...');
179
+ });
180
+
181
+ // Second progress update
182
+ act(() => {
183
+ mockStream.onProgressCallback!({
184
+ status: 'analyzing',
185
+ message: 'Analyzing text...',
186
+ percentage: 50,
187
+ });
188
+ });
189
+
190
+ await waitFor(() => {
191
+ expect(screen.getByTestId('progress')).toHaveTextContent('Analyzing text...');
192
+ });
193
+
194
+ // Final progress update
195
+ act(() => {
196
+ mockStream.onProgressCallback!({
197
+ status: 'complete',
198
+ message: 'Created 14 highlights',
199
+ percentage: 100,
200
+ });
201
+ });
202
+
203
+ await waitFor(() => {
204
+ expect(screen.getByTestId('progress')).toHaveTextContent('Created 14 highlights');
205
+ });
206
+ });
207
+
208
+ it('should keep progress visible after detection completes', async () => {
209
+ const testUri = resourceUri('http://localhost:4000/resources/test-resource');
210
+ const { emitDetectionStart, getEventBus } = renderDetectionFlow(testUri);
211
+
212
+ // Start detection
213
+ act(() => {
214
+ emitDetectionStart('highlighting', { instructions: 'Test' });
215
+ });
216
+
217
+ await waitFor(() => {
218
+ expect(screen.getByTestId('detecting')).toHaveTextContent('highlighting');
219
+ });
220
+
221
+ // Send final progress
222
+ act(() => {
223
+ mockStream.onProgressCallback!({
224
+ status: 'complete',
225
+ message: 'Created 14 highlights',
226
+ });
227
+ });
228
+
229
+ await waitFor(() => {
230
+ expect(screen.getByTestId('progress')).toHaveTextContent('Created 14 highlights');
231
+ });
232
+
233
+ // Emit completion event
234
+ act(() => {
235
+ getEventBus().emit('detection:complete', { motivation: 'highlighting' });
236
+ });
237
+
238
+ // Verify: detecting flag cleared BUT progress still visible
239
+ await waitFor(() => {
240
+ expect(screen.getByTestId('detecting')).toHaveTextContent('none');
241
+ expect(screen.getByTestId('progress')).toHaveTextContent('Created 14 highlights');
242
+ });
243
+ });
244
+
245
+ it('should clear progress on detection failure', async () => {
246
+ const testUri = resourceUri('http://localhost:4000/resources/test-resource');
247
+ const { emitDetectionStart, getEventBus } = renderDetectionFlow(testUri);
248
+
249
+ // Start detection
250
+ act(() => {
251
+ emitDetectionStart('linking', { entityTypes: ['Person'] });
252
+ });
253
+
254
+ // Add some progress
255
+ act(() => {
256
+ mockStream.onProgressCallback!({
257
+ status: 'scanning',
258
+ message: 'Scanning...',
259
+ });
260
+ });
261
+
262
+ await waitFor(() => {
263
+ expect(screen.getByTestId('progress')).toHaveTextContent('Scanning...');
264
+ });
265
+
266
+ // Emit failure
267
+ act(() => {
268
+ getEventBus().emit('detection:failed', { type: 'job.failed', resourceId: 'test-resource' as any, payload: { jobId: 'job-1' as any, jobType: 'detection', error: 'Network error' } });
269
+ });
270
+
271
+ // Verify: both detecting and progress cleared
272
+ await waitFor(() => {
273
+ expect(screen.getByTestId('detecting')).toHaveTextContent('none');
274
+ expect(screen.getByTestId('progress')).toHaveTextContent('No progress');
275
+ });
276
+ });
277
+
278
+ it('should handle different detection motivations with correct API calls', async () => {
279
+ const testUri = resourceUri('http://localhost:4000/resources/test-resource');
280
+ const { emitDetectionStart } = renderDetectionFlow(testUri);
281
+
282
+ // Test highlighting
283
+ act(() => {
284
+ emitDetectionStart('highlighting', { instructions: 'Find important text' });
285
+ });
286
+
287
+ await waitFor(() => {
288
+ expect(detectHighlightsSpy).toHaveBeenCalledTimes(1);
289
+ expect(detectHighlightsSpy).toHaveBeenCalledWith(testUri, {
290
+ instructions: 'Find important text',
291
+ }, { auth: undefined });
292
+ });
293
+
294
+ // Reset for next test
295
+ vi.clearAllMocks();
296
+ mockStream = createMockSSEStream();
297
+ detectCommentsSpy.mockReturnValue(mockStream);
298
+
299
+ // Test commenting
300
+ act(() => {
301
+ emitDetectionStart('commenting', {
302
+ instructions: 'Add helpful comments',
303
+ tone: 'educational'
304
+ });
305
+ });
306
+
307
+ await waitFor(() => {
308
+ expect(detectCommentsSpy).toHaveBeenCalledTimes(1);
309
+ expect(detectCommentsSpy).toHaveBeenCalledWith(testUri, {
310
+ instructions: 'Add helpful comments',
311
+ tone: 'educational',
312
+ }, { auth: undefined });
313
+ });
314
+ });
315
+
316
+ it('should only call API once even with multiple event listeners', async () => {
317
+ const testUri = resourceUri('http://localhost:4000/resources/test-resource');
318
+
319
+ // This test specifically catches the duplicate useEventOperations bug
320
+ // If multiple components call useEventOperations, we'll see multiple API calls
321
+ const { emitDetectionStart, getEventBus } = renderDetectionFlow(testUri);
322
+
323
+ // Add an additional event listener (simulating multiple subscribers)
324
+ const additionalListener = vi.fn();
325
+ getEventBus().on('detection:start', additionalListener);
326
+
327
+ // Trigger detection
328
+ act(() => {
329
+ emitDetectionStart('linking', { entityTypes: ['Person'] });
330
+ });
331
+
332
+ // Wait for operation to complete
333
+ await waitFor(() => {
334
+ expect(detectAnnotationsSpy).toHaveBeenCalled();
335
+ });
336
+
337
+ // VERIFY: API called exactly once, even though multiple listeners exist
338
+ expect(detectAnnotationsSpy).toHaveBeenCalledTimes(1);
339
+
340
+ // VERIFY: Our additional listener was called (events work)
341
+ expect(additionalListener).toHaveBeenCalledTimes(1);
342
+ });
343
+ });
344
+
345
+ /**
346
+ * Helper: Render useDetectionFlow hook with real component composition
347
+ * Returns methods to interact with the rendered component
348
+ */
349
+ function renderDetectionFlow(testUri: string) {
350
+ let eventBusInstance: Emitter<EventMap>;
351
+
352
+ // Component to capture EventBus instance
353
+ function EventBusCapture() {
354
+ eventBusInstance = useEventBus();
355
+ return null;
356
+ }
357
+
358
+ // Test harness component that uses the hook
359
+ function DetectionFlowTestHarness() {
360
+ const { detectionProgress, detectingMotivation } = useDetectionFlow(testUri as any);
361
+ return (
362
+ <div>
363
+ <div data-testid="detecting">{detectingMotivation || 'none'}</div>
364
+ <div data-testid="progress">
365
+ {detectionProgress?.message || 'No progress'}
366
+ </div>
367
+ </div>
368
+ );
369
+ }
370
+
371
+ render(
372
+ <EventBusProvider>
373
+ <AuthTokenProvider token={null}>
374
+ <ApiClientProvider baseUrl="http://localhost:4000">
375
+ <EventBusCapture />
376
+ <DetectionFlowTestHarness />
377
+ </ApiClientProvider>
378
+ </AuthTokenProvider>
379
+ </EventBusProvider>
380
+ );
381
+
382
+ return {
383
+ emitDetectionStart: (motivation: Motivation, options: any) => {
384
+ eventBusInstance.emit('detection:start', { motivation, options });
385
+ },
386
+ getEventBus: () => eventBusInstance,
387
+ };
388
+ }