@semiont/react-ui 0.2.33-build.78 → 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 (219) 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-RNNSPLQB.mjs → ar-4ZEORRW2.mjs} +8 -4
  5. package/dist/ar-4ZEORRW2.mjs.map +1 -0
  6. package/dist/{bn-S2CDL7EC.mjs → bn-SEDE5BQJ.mjs} +8 -4
  7. package/dist/bn-SEDE5BQJ.mjs.map +1 -0
  8. package/dist/{chunk-UDX2Q35T.mjs → chunk-D7NBW4RV.mjs} +8 -4
  9. package/dist/chunk-D7NBW4RV.mjs.map +1 -0
  10. package/dist/{chunk-35LLVRFK.mjs → chunk-ZR4ZV2LY.mjs} +206 -146
  11. package/dist/chunk-ZR4ZV2LY.mjs.map +1 -0
  12. package/dist/{cs-RSV675WU.mjs → cs-7W4WF5WD.mjs} +8 -4
  13. package/dist/cs-7W4WF5WD.mjs.map +1 -0
  14. package/dist/{da-CHXNPWJC.mjs → da-75XGBCBK.mjs} +8 -4
  15. package/dist/da-75XGBCBK.mjs.map +1 -0
  16. package/dist/{de-KPEZ53D4.mjs → de-ODJVFLHM.mjs} +8 -4
  17. package/dist/de-ODJVFLHM.mjs.map +1 -0
  18. package/dist/{el-MW2BME5T.mjs → el-C4PM4WB3.mjs} +8 -4
  19. package/dist/el-C4PM4WB3.mjs.map +1 -0
  20. package/dist/{en-EVMIX24Y.mjs → en-KJCJQ4OO.mjs} +2 -2
  21. package/dist/{es-HQ24NYS3.mjs → es-WD33R7QL.mjs} +8 -4
  22. package/dist/es-WD33R7QL.mjs.map +1 -0
  23. package/dist/{fa-W34LRLHG.mjs → fa-2BP6V56P.mjs} +8 -4
  24. package/dist/fa-2BP6V56P.mjs.map +1 -0
  25. package/dist/{fi-3U44IGOA.mjs → fi-USRRW24J.mjs} +8 -4
  26. package/dist/fi-USRRW24J.mjs.map +1 -0
  27. package/dist/{fr-N7DKX6NN.mjs → fr-EC5S6WVF.mjs} +8 -4
  28. package/dist/fr-EC5S6WVF.mjs.map +1 -0
  29. package/dist/{he-CS4WRXN3.mjs → he-7TBVIKAA.mjs} +8 -4
  30. package/dist/he-7TBVIKAA.mjs.map +1 -0
  31. package/dist/{hi-GJDY46KA.mjs → hi-FO4VIZLA.mjs} +8 -4
  32. package/dist/hi-FO4VIZLA.mjs.map +1 -0
  33. package/dist/{id-WAEZJK2Y.mjs → id-7U7GGVWY.mjs} +8 -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 +699 -529
  38. package/dist/index.mjs +4291 -3491
  39. package/dist/index.mjs.map +1 -1
  40. package/dist/{it-VDNDMZPU.mjs → it-Y4OPL6I2.mjs} +8 -4
  41. package/dist/it-Y4OPL6I2.mjs.map +1 -0
  42. package/dist/{ja-5PEH56J5.mjs → ja-PK7SQL55.mjs} +8 -4
  43. package/dist/ja-PK7SQL55.mjs.map +1 -0
  44. package/dist/{ko-JYPL3WVA.mjs → ko-L25PXMYD.mjs} +8 -4
  45. package/dist/ko-L25PXMYD.mjs.map +1 -0
  46. package/dist/{ms-5PZVW76T.mjs → ms-STH777QM.mjs} +8 -4
  47. package/dist/ms-STH777QM.mjs.map +1 -0
  48. package/dist/{nl-YXES36KM.mjs → nl-Y7LECDDR.mjs} +8 -4
  49. package/dist/nl-Y7LECDDR.mjs.map +1 -0
  50. package/dist/{no-XRA2UCQD.mjs → no-KEKCEWU6.mjs} +8 -4
  51. package/dist/no-KEKCEWU6.mjs.map +1 -0
  52. package/dist/{pl-WH6LJA5G.mjs → pl-7A7OC75O.mjs} +8 -4
  53. package/dist/pl-7A7OC75O.mjs.map +1 -0
  54. package/dist/{pt-7GAG57BM.mjs → pt-35HTM7RA.mjs} +8 -4
  55. package/dist/pt-35HTM7RA.mjs.map +1 -0
  56. package/dist/{ro-BTDDRB7N.mjs → ro-VAWL5KQA.mjs} +8 -4
  57. package/dist/ro-VAWL5KQA.mjs.map +1 -0
  58. package/dist/{sv-7V5C2IT4.mjs → sv-7ZK5EQEB.mjs} +8 -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-LPKYLBX5.mjs → th-UDWZ4X34.mjs} +8 -4
  64. package/dist/th-UDWZ4X34.mjs.map +1 -0
  65. package/dist/{tr-DU4RQL4M.mjs → tr-4WMPK3UX.mjs} +8 -4
  66. package/dist/tr-4WMPK3UX.mjs.map +1 -0
  67. package/dist/{uk-36UHTDDI.mjs → uk-SSLASQYJ.mjs} +8 -4
  68. package/dist/uk-SSLASQYJ.mjs.map +1 -0
  69. package/dist/{vi-GDHOUZDH.mjs → vi-IF42Z5PU.mjs} +8 -4
  70. package/dist/vi-IF42Z5PU.mjs.map +1 -0
  71. package/dist/{zh-TYUID4XZ.mjs → zh-HRQTNTAI.mjs} +8 -4
  72. package/dist/zh-HRQTNTAI.mjs.map +1 -0
  73. package/package.json +8 -2
  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 -138
  100. package/src/components/resource/AnnotationHistory.tsx +12 -13
  101. package/src/components/resource/BrowseView.tsx +89 -177
  102. package/src/components/resource/HistoryEvent.tsx +16 -11
  103. package/src/components/resource/ResourceViewer.tsx +201 -370
  104. package/src/components/resource/__tests__/BrowseView.test.tsx +631 -0
  105. package/src/components/resource/__tests__/ResourceViewer.mode-switch.test.tsx +231 -0
  106. package/src/components/resource/event-formatting.ts +316 -0
  107. package/src/components/resource/panels/AssessmentEntry.tsx +25 -33
  108. package/src/components/resource/panels/AssessmentPanel.tsx +137 -31
  109. package/src/components/resource/panels/CollaborationPanel.tsx +20 -13
  110. package/src/components/resource/panels/CommentEntry.tsx +38 -32
  111. package/src/components/resource/panels/CommentsPanel.tsx +153 -31
  112. package/src/components/resource/panels/DetectSection.css +36 -1
  113. package/src/components/resource/panels/DetectSection.tsx +38 -10
  114. package/src/components/resource/panels/HighlightEntry.tsx +25 -33
  115. package/src/components/resource/panels/HighlightPanel.tsx +100 -25
  116. package/src/components/resource/panels/ReferenceEntry.tsx +61 -75
  117. package/src/components/resource/panels/ReferencesPanel.tsx +166 -49
  118. package/src/components/resource/panels/ResourceInfoPanel.tsx +47 -48
  119. package/src/components/resource/panels/StatisticsPanel.tsx +9 -19
  120. package/src/components/resource/panels/TagEntry.tsx +25 -33
  121. package/src/components/resource/panels/TaggingPanel.tsx +141 -25
  122. package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +46 -101
  123. package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +566 -0
  124. package/src/components/resource/panels/__tests__/CommentEntry.test.tsx +86 -78
  125. package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +146 -141
  126. package/src/components/resource/panels/__tests__/DetectSection.test.tsx +480 -0
  127. package/src/components/resource/panels/__tests__/HighlightPanel.detectionProgress.test.tsx +362 -0
  128. package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +228 -103
  129. package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +117 -61
  130. package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +586 -0
  131. package/src/components/settings/SettingsPanel.tsx +15 -12
  132. package/src/features/admin-devops/__tests__/AdminDevOpsPage.test.tsx +1 -46
  133. package/src/features/admin-devops/components/AdminDevOpsPage.tsx +0 -9
  134. package/src/features/admin-security/__tests__/AdminSecurityPage.test.tsx +0 -3
  135. package/src/features/admin-security/components/AdminSecurityPage.tsx +0 -9
  136. package/src/features/admin-users/__tests__/AdminUsersPage.test.tsx +0 -3
  137. package/src/features/admin-users/components/AdminUsersPage.tsx +0 -9
  138. package/src/features/moderate-entity-tags/__tests__/EntityTagsPage.test.tsx +0 -3
  139. package/src/features/moderate-entity-tags/components/EntityTagsPage.tsx +1 -9
  140. package/src/features/moderate-recent/__tests__/RecentDocumentsPage.test.tsx +0 -32
  141. package/src/features/moderate-recent/components/RecentDocumentsPage.tsx +1 -9
  142. package/src/features/moderate-tag-schemas/__tests__/TagSchemasPage.test.tsx +0 -32
  143. package/src/features/moderate-tag-schemas/components/TagSchemasPage.tsx +1 -9
  144. package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +51 -54
  145. package/src/features/resource-compose/components/ResourceComposePage.tsx +3 -13
  146. package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +39 -45
  147. package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +16 -27
  148. package/src/features/resource-viewer/__tests__/AnnotationDeletionIntegration.test.tsx +231 -0
  149. package/src/features/resource-viewer/__tests__/DetectionFlowBug.test.tsx +234 -0
  150. package/src/features/resource-viewer/__tests__/DetectionFlowIntegration.test.tsx +388 -0
  151. package/src/features/resource-viewer/__tests__/DetectionProgressDismissal.test.tsx +318 -0
  152. package/src/features/resource-viewer/__tests__/GenerationFlowIntegration.test.tsx +504 -0
  153. package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +145 -91
  154. package/src/features/resource-viewer/__tests__/detection-progress-flow.test.tsx +322 -0
  155. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +325 -476
  156. package/src/styles/motivations/motivation-assessment.css +28 -0
  157. package/src/styles/patterns/panel-helpers.css +26 -0
  158. package/translations/ar.json +7 -3
  159. package/translations/bn.json +7 -3
  160. package/translations/cs.json +7 -3
  161. package/translations/da.json +7 -3
  162. package/translations/de.json +7 -3
  163. package/translations/el.json +7 -3
  164. package/translations/en.json +7 -3
  165. package/translations/es.json +7 -3
  166. package/translations/fa.json +7 -3
  167. package/translations/fi.json +7 -3
  168. package/translations/fr.json +7 -3
  169. package/translations/he.json +7 -3
  170. package/translations/hi.json +7 -3
  171. package/translations/id.json +7 -3
  172. package/translations/it.json +7 -3
  173. package/translations/ja.json +7 -3
  174. package/translations/ko.json +7 -3
  175. package/translations/ms.json +7 -3
  176. package/translations/nl.json +7 -3
  177. package/translations/no.json +7 -3
  178. package/translations/pl.json +7 -3
  179. package/translations/pt.json +7 -3
  180. package/translations/ro.json +7 -3
  181. package/translations/sv.json +7 -3
  182. package/translations/th.json +7 -3
  183. package/translations/tr.json +7 -3
  184. package/translations/uk.json +7 -3
  185. package/translations/vi.json +7 -3
  186. package/translations/zh.json +7 -3
  187. package/dist/PdfAnnotationCanvas.client-ADC4FFSE.mjs.map +0 -1
  188. package/dist/TranslationManager-Co_5fSxl.d.mts +0 -118
  189. package/dist/ar-RNNSPLQB.mjs.map +0 -1
  190. package/dist/bn-S2CDL7EC.mjs.map +0 -1
  191. package/dist/chunk-35LLVRFK.mjs.map +0 -1
  192. package/dist/chunk-UDX2Q35T.mjs.map +0 -1
  193. package/dist/cs-RSV675WU.mjs.map +0 -1
  194. package/dist/da-CHXNPWJC.mjs.map +0 -1
  195. package/dist/de-KPEZ53D4.mjs.map +0 -1
  196. package/dist/el-MW2BME5T.mjs.map +0 -1
  197. package/dist/es-HQ24NYS3.mjs.map +0 -1
  198. package/dist/fa-W34LRLHG.mjs.map +0 -1
  199. package/dist/fi-3U44IGOA.mjs.map +0 -1
  200. package/dist/fr-N7DKX6NN.mjs.map +0 -1
  201. package/dist/he-CS4WRXN3.mjs.map +0 -1
  202. package/dist/hi-GJDY46KA.mjs.map +0 -1
  203. package/dist/id-WAEZJK2Y.mjs.map +0 -1
  204. package/dist/it-VDNDMZPU.mjs.map +0 -1
  205. package/dist/ja-5PEH56J5.mjs.map +0 -1
  206. package/dist/ko-JYPL3WVA.mjs.map +0 -1
  207. package/dist/ms-5PZVW76T.mjs.map +0 -1
  208. package/dist/nl-YXES36KM.mjs.map +0 -1
  209. package/dist/no-XRA2UCQD.mjs.map +0 -1
  210. package/dist/pl-WH6LJA5G.mjs.map +0 -1
  211. package/dist/pt-7GAG57BM.mjs.map +0 -1
  212. package/dist/ro-BTDDRB7N.mjs.map +0 -1
  213. package/dist/sv-7V5C2IT4.mjs.map +0 -1
  214. package/dist/th-LPKYLBX5.mjs.map +0 -1
  215. package/dist/tr-DU4RQL4M.mjs.map +0 -1
  216. package/dist/uk-36UHTDDI.mjs.map +0 -1
  217. package/dist/vi-GDHOUZDH.mjs.map +0 -1
  218. package/dist/zh-TYUID4XZ.mjs.map +0 -1
  219. /package/dist/{en-EVMIX24Y.mjs.map → en-KJCJQ4OO.mjs.map} +0 -0
@@ -0,0 +1,504 @@
1
+ /**
2
+ * Layer 3: Feature Integration Test - Generation Flow Architecture
3
+ *
4
+ * Tests the COMPLETE generation flow with real component composition:
5
+ * - EventBusProvider (REAL)
6
+ * - ApiClientProvider (REAL, with MOCKED client)
7
+ * - useGenerationFlow (REAL)
8
+ * - useGenerationProgress (REAL)
9
+ * - useEventOperations (REAL)
10
+ * - useEventSubscriptions (REAL)
11
+ *
12
+ * This test focuses on ARCHITECTURE and EVENT WIRING:
13
+ * - Verifies API called exactly ONCE (catches duplicate subscriptions)
14
+ * - Tests event propagation through the event bus
15
+ * - Validates modal workflow (open → submit → SSE stream)
16
+ * - Ensures generation progress updates correctly
17
+ * - Tests success/error handling
18
+ *
19
+ * NO BACKEND SERVER - only mocked API client boundary
20
+ */
21
+
22
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
23
+ import { render, screen, waitFor } from '@testing-library/react';
24
+ import { act } from 'react';
25
+ import { useGenerationFlow } from '../../../hooks/useGenerationFlow';
26
+ import { EventBusProvider, useEventBus, resetEventBusForTesting } from '../../../contexts/EventBusContext';
27
+ import { ApiClientProvider, useApiClient } from '../../../contexts/ApiClientContext';
28
+ import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
29
+ import { useEventOperations } from '../../../contexts/useEventOperations';
30
+ import { SSEClient } from '@semiont/api-client';
31
+ import type { SemiontApiClient, ResourceUri, AnnotationUri } from '@semiont/api-client';
32
+ import { resourceUri, annotationUri } from '@semiont/api-client';
33
+ import type { Emitter } from 'mitt';
34
+ import type { EventMap } from '../../../contexts/EventBusContext';
35
+
36
+ // Mock SSE stream that we can control in tests
37
+ const createMockGenerationStream = () => {
38
+ const stream = {
39
+ onProgressCallback: null as ((chunk: any) => void) | null,
40
+ onCompleteCallback: null as ((finalChunk: any) => void) | null,
41
+ onErrorCallback: null as ((error: Error) => void) | null,
42
+ onProgress: vi.fn((callback: (chunk: any) => void) => {
43
+ stream.onProgressCallback = callback;
44
+ return stream;
45
+ }),
46
+ onComplete: vi.fn((callback: (finalChunk: any) => void) => {
47
+ stream.onCompleteCallback = callback;
48
+ return stream;
49
+ }),
50
+ onError: vi.fn((callback: (error: Error) => void) => {
51
+ stream.onErrorCallback = callback;
52
+ return stream;
53
+ }),
54
+ close: vi.fn(),
55
+ };
56
+ return stream;
57
+ };
58
+
59
+ describe('Generation Flow - Feature Integration', () => {
60
+ let mockStream: ReturnType<typeof createMockGenerationStream>;
61
+ let generateResourceSpy: any;
62
+ let mockShowSuccess: ReturnType<typeof vi.fn>;
63
+ let mockShowError: ReturnType<typeof vi.fn>;
64
+ let mockCacheManager: { invalidate: ReturnType<typeof vi.fn> };
65
+
66
+ beforeEach(() => {
67
+ vi.clearAllMocks();
68
+ resetEventBusForTesting();
69
+
70
+ // Create fresh mock stream for each test
71
+ mockStream = createMockGenerationStream();
72
+
73
+ // Spy on SSEClient prototype method
74
+ generateResourceSpy = vi.spyOn(SSEClient.prototype, 'generateResourceFromAnnotation').mockReturnValue(mockStream as any);
75
+
76
+ // Mock callbacks
77
+ mockShowSuccess = vi.fn();
78
+ mockShowError = vi.fn();
79
+ mockCacheManager = { invalidate: vi.fn() };
80
+ });
81
+
82
+ afterEach(() => {
83
+ vi.restoreAllMocks();
84
+ });
85
+
86
+ it('should open modal when generation:modal-open event is emitted', async () => {
87
+ const testResourceUri = resourceUri('http://localhost:4000/resources/test-resource');
88
+ const testAnnotationUri = annotationUri('http://localhost:4000/resources/test-resource/annotations/test-annotation');
89
+
90
+ const { emitModalOpen } = renderGenerationFlow(
91
+ testResourceUri
92
+ );
93
+
94
+ // Emit modal open event
95
+ act(() => {
96
+ emitModalOpen(testAnnotationUri, testResourceUri, 'Test Reference');
97
+ });
98
+
99
+ // Verify modal state updated
100
+ await waitFor(() => {
101
+ expect(screen.getByTestId('modal-open')).toHaveTextContent('true');
102
+ expect(screen.getByTestId('reference-id')).toHaveTextContent(testAnnotationUri);
103
+ expect(screen.getByTestId('default-title')).toHaveTextContent('Test Reference');
104
+ });
105
+ });
106
+
107
+ it('should call generateResourceFromAnnotation exactly ONCE when generation starts', async () => {
108
+ const testResourceUri = resourceUri('http://localhost:4000/resources/test-resource');
109
+ const testAnnotationUri = annotationUri('http://localhost:4000/resources/test-resource/annotations/test-annotation');
110
+
111
+ const { emitGenerationStart } = renderGenerationFlow(
112
+ testResourceUri
113
+ );
114
+
115
+ // Trigger generation with full options
116
+ act(() => {
117
+ emitGenerationStart(testAnnotationUri, testResourceUri, {
118
+ title: 'Generated Document',
119
+ prompt: 'Create a comprehensive document',
120
+ language: 'en',
121
+ temperature: 0.7,
122
+ maxTokens: 2000,
123
+ context: {
124
+ sourceText: 'Reference text from the document',
125
+ entityTypes: ['Person', 'Organization'],
126
+ },
127
+ });
128
+ });
129
+
130
+ // CRITICAL ASSERTION: API called exactly once (not twice!)
131
+ await waitFor(() => {
132
+ expect(generateResourceSpy).toHaveBeenCalledTimes(1);
133
+ });
134
+
135
+ // Verify correct parameters
136
+ expect(generateResourceSpy).toHaveBeenCalledWith(
137
+ testResourceUri,
138
+ testAnnotationUri,
139
+ {
140
+ title: 'Generated Document',
141
+ prompt: 'Create a comprehensive document',
142
+ language: 'en',
143
+ temperature: 0.7,
144
+ maxTokens: 2000,
145
+ context: {
146
+ sourceText: 'Reference text from the document',
147
+ entityTypes: ['Person', 'Organization'],
148
+ },
149
+ },
150
+ { auth: undefined }
151
+ );
152
+ });
153
+
154
+ it('should propagate SSE progress events to useGenerationProgress state', async () => {
155
+ const testResourceUri = resourceUri('http://localhost:4000/resources/test-resource');
156
+ const testAnnotationUri = annotationUri('http://localhost:4000/resources/test-resource/annotations/test-annotation');
157
+
158
+ const { emitGenerationStart } = renderGenerationFlow(
159
+ testResourceUri
160
+ );
161
+
162
+ // Start generation
163
+ act(() => {
164
+ emitGenerationStart(testAnnotationUri, testResourceUri, {
165
+ title: 'Test Doc',
166
+ context: { sourceText: 'test' },
167
+ });
168
+ });
169
+
170
+ // Wait for stream to be created
171
+ await waitFor(() => {
172
+ expect(generateResourceSpy).toHaveBeenCalled();
173
+ });
174
+
175
+ // Simulate SSE progress callback being invoked
176
+ act(() => {
177
+ mockStream.onProgressCallback!({
178
+ status: 'generating',
179
+ message: 'Generating content...',
180
+ percentage: 25,
181
+ });
182
+ });
183
+
184
+ // Verify progress propagated to UI
185
+ await waitFor(() => {
186
+ expect(screen.getByTestId('progress')).toHaveTextContent('Generating content...');
187
+ expect(screen.getByTestId('is-generating')).toHaveTextContent('true');
188
+ });
189
+ });
190
+
191
+ it('should handle multiple progress updates correctly', async () => {
192
+ const testResourceUri = resourceUri('http://localhost:4000/resources/test-resource');
193
+ const testAnnotationUri = annotationUri('http://localhost:4000/resources/test-resource/annotations/test-annotation');
194
+
195
+ const { emitGenerationStart } = renderGenerationFlow(
196
+ testResourceUri
197
+ );
198
+
199
+ // Start generation
200
+ act(() => {
201
+ emitGenerationStart(testAnnotationUri, testResourceUri, {
202
+ title: 'Test',
203
+ context: { sourceText: 'test' },
204
+ });
205
+ });
206
+
207
+ await waitFor(() => {
208
+ expect(generateResourceSpy).toHaveBeenCalledTimes(1);
209
+ });
210
+
211
+ // First progress update
212
+ act(() => {
213
+ mockStream.onProgressCallback!({
214
+ status: 'started',
215
+ message: 'Starting generation...',
216
+ percentage: 0,
217
+ });
218
+ });
219
+
220
+ await waitFor(() => {
221
+ expect(screen.getByTestId('progress')).toHaveTextContent('Starting generation...');
222
+ });
223
+
224
+ // Second progress update
225
+ act(() => {
226
+ mockStream.onProgressCallback!({
227
+ status: 'generating',
228
+ message: 'Creating document structure...',
229
+ percentage: 50,
230
+ });
231
+ });
232
+
233
+ await waitFor(() => {
234
+ expect(screen.getByTestId('progress')).toHaveTextContent('Creating document structure...');
235
+ });
236
+
237
+ // Final progress update via onComplete
238
+ act(() => {
239
+ mockStream.onCompleteCallback!({
240
+ status: 'complete',
241
+ message: 'Document created successfully',
242
+ percentage: 100,
243
+ resourceName: 'Generated Document',
244
+ });
245
+ });
246
+
247
+ await waitFor(() => {
248
+ expect(screen.getByTestId('progress')).toHaveTextContent('Document created successfully');
249
+ // Progress stays visible after completion (like detection flow)
250
+ });
251
+ });
252
+
253
+ it('should show success toast on generation complete', async () => {
254
+ const testResourceUri = resourceUri('http://localhost:4000/resources/test-resource');
255
+ const testAnnotationUri = annotationUri('http://localhost:4000/resources/test-resource/annotations/test-annotation');
256
+
257
+ const { emitGenerationStart, getEventBus } = renderGenerationFlow(
258
+ testResourceUri
259
+ );
260
+
261
+ // Start generation
262
+ act(() => {
263
+ emitGenerationStart(testAnnotationUri, testResourceUri, {
264
+ title: 'Test',
265
+ context: { sourceText: 'test' },
266
+ });
267
+ });
268
+
269
+ await waitFor(() => {
270
+ expect(generateResourceSpy).toHaveBeenCalled();
271
+ });
272
+
273
+ // Simulate completion with final chunk
274
+ act(() => {
275
+ mockStream.onProgressCallback!({
276
+ status: 'complete',
277
+ message: 'Complete',
278
+ resourceName: 'Generated Document',
279
+ });
280
+ });
281
+
282
+ // Emit completion event
283
+ act(() => {
284
+ getEventBus().emit('generation:complete', {
285
+ annotationUri: testAnnotationUri,
286
+ progress: {
287
+ status: 'complete',
288
+ resourceName: 'Generated Document',
289
+ },
290
+ });
291
+ });
292
+
293
+ // Verify generation completes successfully
294
+ // Note: Progress stays visible after completion (like detection flow)
295
+ });
296
+
297
+ it('should clear progress on generation failure', async () => {
298
+ const testResourceUri = resourceUri('http://localhost:4000/resources/test-resource');
299
+ const testAnnotationUri = annotationUri('http://localhost:4000/resources/test-resource/annotations/test-annotation');
300
+
301
+ const { emitGenerationStart, getEventBus } = renderGenerationFlow(
302
+ testResourceUri
303
+ );
304
+
305
+ // Start generation
306
+ act(() => {
307
+ emitGenerationStart(testAnnotationUri, testResourceUri, {
308
+ title: 'Test',
309
+ context: { sourceText: 'test' },
310
+ });
311
+ });
312
+
313
+ // Add some progress
314
+ act(() => {
315
+ mockStream.onProgressCallback!({
316
+ status: 'generating',
317
+ message: 'Generating...',
318
+ });
319
+ });
320
+
321
+ await waitFor(() => {
322
+ expect(screen.getByTestId('progress')).toHaveTextContent('Generating...');
323
+ });
324
+
325
+ // Emit failure
326
+ act(() => {
327
+ getEventBus().emit('generation:failed', { error: new Error('Network error') });
328
+ });
329
+
330
+ // Verify: progress cleared and not generating
331
+ await waitFor(() => {
332
+ expect(screen.getByTestId('is-generating')).toHaveTextContent('false');
333
+ expect(screen.getByTestId('progress')).toHaveTextContent('No progress');
334
+ });
335
+ });
336
+
337
+ it('should only call API once even with multiple event listeners', async () => {
338
+ const testResourceUri = resourceUri('http://localhost:4000/resources/test-resource');
339
+ const testAnnotationUri = annotationUri('http://localhost:4000/resources/test-resource/annotations/test-annotation');
340
+
341
+ const { emitGenerationStart, getEventBus } = renderGenerationFlow(
342
+ testResourceUri
343
+ );
344
+
345
+ // Add an additional event listener (simulating multiple subscribers)
346
+ const additionalListener = vi.fn();
347
+ getEventBus().on('generation:start', additionalListener);
348
+
349
+ // Trigger generation
350
+ act(() => {
351
+ emitGenerationStart(testAnnotationUri, testResourceUri, {
352
+ title: 'Test',
353
+ context: { sourceText: 'test' },
354
+ });
355
+ });
356
+
357
+ // Wait for operation to complete
358
+ await waitFor(() => {
359
+ expect(generateResourceSpy).toHaveBeenCalled();
360
+ });
361
+
362
+ // VERIFY: API called exactly once, even though multiple listeners exist
363
+ expect(generateResourceSpy).toHaveBeenCalledTimes(1);
364
+
365
+ // VERIFY: Our additional listener was called (events work)
366
+ expect(additionalListener).toHaveBeenCalledTimes(1);
367
+ });
368
+
369
+ it('should forward final chunk as progress before emitting complete', async () => {
370
+ const testResourceUri = resourceUri('http://localhost:4000/resources/test-resource');
371
+ const testAnnotationUri = annotationUri('http://localhost:4000/resources/test-resource/annotations/test-annotation');
372
+
373
+ const { emitGenerationStart } = renderGenerationFlow(
374
+ testResourceUri
375
+ );
376
+
377
+ // Start generation
378
+ act(() => {
379
+ emitGenerationStart(testAnnotationUri, testResourceUri, {
380
+ title: 'Test',
381
+ context: { sourceText: 'test' },
382
+ });
383
+ });
384
+
385
+ await waitFor(() => {
386
+ expect(generateResourceSpy).toHaveBeenCalled();
387
+ });
388
+
389
+ // Simulate onComplete with final chunk
390
+ act(() => {
391
+ mockStream.onCompleteCallback!({
392
+ status: 'complete',
393
+ message: 'Document created: My Document',
394
+ resourceName: 'My Document',
395
+ percentage: 100,
396
+ });
397
+ });
398
+
399
+ // Verify final chunk is visible as progress
400
+ await waitFor(() => {
401
+ expect(screen.getByTestId('progress')).toHaveTextContent('Document created: My Document');
402
+ // Progress stays visible after completion (like detection flow)
403
+ });
404
+ });
405
+ });
406
+
407
+ /**
408
+ * Helper: Render useGenerationFlow hook with real component composition
409
+ * Returns methods to interact with the rendered component
410
+ */
411
+ function renderGenerationFlow(
412
+ testResourceUri: ResourceUri
413
+ ) {
414
+ let eventBusInstance: Emitter<EventMap>;
415
+
416
+ // Component to capture EventBus instance and set up event operations
417
+ function EventBusCapture() {
418
+ eventBusInstance = useEventBus();
419
+ const client = useApiClient();
420
+
421
+ // Set up event operations (this is what makes the SSE calls)
422
+ useEventOperations(eventBusInstance, {
423
+ client: client as SemiontApiClient,
424
+ resourceUri: testResourceUri,
425
+ });
426
+
427
+ return null;
428
+ }
429
+
430
+ // Test harness component that uses the hook
431
+ function GenerationFlowTestHarness() {
432
+ const {
433
+ generationProgress,
434
+ generationModalOpen,
435
+ generationReferenceId,
436
+ generationDefaultTitle,
437
+ } = useGenerationFlow(
438
+ 'en',
439
+ testResourceUri.split('/resources/')[1] || 'test-resource',
440
+ vi.fn(),
441
+ vi.fn(),
442
+ null,
443
+ vi.fn()
444
+ );
445
+
446
+ return (
447
+ <div>
448
+ <div data-testid="modal-open">{generationModalOpen ? 'true' : 'false'}</div>
449
+ <div data-testid="reference-id">{generationReferenceId || 'none'}</div>
450
+ <div data-testid="default-title">{generationDefaultTitle || 'none'}</div>
451
+ <div data-testid="is-generating">
452
+ {generationProgress ? 'true' : 'false'}
453
+ </div>
454
+ <div data-testid="progress">
455
+ {generationProgress?.message || 'No progress'}
456
+ </div>
457
+ </div>
458
+ );
459
+ }
460
+
461
+ render(
462
+ <EventBusProvider>
463
+ <AuthTokenProvider token={null}>
464
+ <ApiClientProvider baseUrl="http://localhost:4000">
465
+ <EventBusCapture />
466
+ <GenerationFlowTestHarness />
467
+ </ApiClientProvider>
468
+ </AuthTokenProvider>
469
+ </EventBusProvider>
470
+ );
471
+
472
+ return {
473
+ emitModalOpen: (
474
+ annotationUri: AnnotationUri,
475
+ resourceUri: ResourceUri,
476
+ defaultTitle: string
477
+ ) => {
478
+ eventBusInstance.emit('generation:modal-open', {
479
+ annotationUri,
480
+ resourceUri,
481
+ defaultTitle,
482
+ });
483
+ },
484
+ emitGenerationStart: (
485
+ annotationUri: AnnotationUri,
486
+ resourceUri: ResourceUri,
487
+ options: {
488
+ title: string;
489
+ prompt?: string;
490
+ language?: string;
491
+ temperature?: number;
492
+ maxTokens?: number;
493
+ context: any;
494
+ }
495
+ ) => {
496
+ eventBusInstance.emit('generation:start', {
497
+ annotationUri,
498
+ resourceUri,
499
+ options,
500
+ });
501
+ },
502
+ getEventBus: () => eventBusInstance,
503
+ };
504
+ }