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

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