@semiont/react-ui 0.4.20 → 0.4.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/README.md +8 -5
  2. package/dist/{PdfAnnotationCanvas.client-CHDCGQBR.mjs → PdfAnnotationCanvas.client-6ZGFEN2N.mjs} +9 -13
  3. package/dist/PdfAnnotationCanvas.client-6ZGFEN2N.mjs.map +1 -0
  4. package/dist/TranslationManager-9Xj3MIWQ.d.mts +16 -0
  5. package/dist/chunk-KEDFYI6N.mjs +7788 -0
  6. package/dist/chunk-KEDFYI6N.mjs.map +1 -0
  7. package/dist/index.d.mts +171 -1140
  8. package/dist/index.mjs +3263 -13644
  9. package/dist/index.mjs.map +1 -1
  10. package/dist/test-utils.d.mts +46 -21
  11. package/dist/test-utils.mjs +2499 -87
  12. package/dist/test-utils.mjs.map +1 -1
  13. package/package.json +1 -2
  14. package/src/components/AnnotateReferencesProgressWidget.tsx +21 -28
  15. package/src/components/CodeMirrorRenderer.tsx +9 -11
  16. package/src/components/StatusDisplay.tsx +42 -16
  17. package/src/components/Toolbar.tsx +4 -4
  18. package/src/components/__tests__/AnnotateReferencesProgressWidget.test.tsx +34 -20
  19. package/src/components/__tests__/StatusDisplay.test.tsx +47 -64
  20. package/src/components/__tests__/Toolbar.test.tsx +4 -4
  21. package/src/components/annotation/AnnotateToolbar.tsx +8 -7
  22. package/src/components/annotation/__tests__/AnnotateToolbar.test.tsx +31 -77
  23. package/src/components/annotation-popups/__tests__/JsonLdView.test.tsx +0 -1
  24. package/src/components/image-annotation/AnnotationOverlay.tsx +12 -13
  25. package/src/components/image-annotation/SvgDrawingCanvas.tsx +7 -7
  26. package/src/components/modals/PermissionDeniedModal.tsx +11 -11
  27. package/src/components/modals/ReferenceWizardModal.tsx +14 -18
  28. package/src/components/modals/ResourceSearchModal.tsx +10 -6
  29. package/src/components/modals/SearchModal.tsx +10 -6
  30. package/src/components/modals/SessionExpiredModal.tsx +11 -11
  31. package/src/components/modals/__tests__/PermissionDeniedModal.test.tsx +7 -7
  32. package/src/components/modals/__tests__/ResourceSearchModal.test.tsx +10 -8
  33. package/src/components/modals/__tests__/SearchModal.search-wiring.test.tsx +10 -7
  34. package/src/components/modals/__tests__/SessionExpiredModal.test.tsx +5 -5
  35. package/src/components/navigation/CollapsibleResourceNavigation.tsx +10 -10
  36. package/src/components/navigation/ObservableLink.tsx +6 -6
  37. package/src/components/navigation/SimpleNavigation.tsx +4 -4
  38. package/src/components/navigation/__tests__/ObservableLink.test.tsx +4 -4
  39. package/src/components/navigation/__tests__/SimpleNavigation.test.tsx +4 -4
  40. package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +9 -11
  41. package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +0 -1
  42. package/src/components/resource/AnnotateView.tsx +7 -6
  43. package/src/components/resource/AnnotationHistory.tsx +9 -12
  44. package/src/components/resource/BrowseView.tsx +8 -7
  45. package/src/components/resource/ResourceViewer.tsx +17 -25
  46. package/src/components/resource/__tests__/AnnotationHistory.test.tsx +54 -192
  47. package/src/components/resource/__tests__/BrowseView.test.tsx +34 -83
  48. package/src/components/resource/__tests__/ResourceViewer.mode-switch.test.tsx +40 -31
  49. package/src/components/resource/panels/AssessmentEntry.tsx +5 -4
  50. package/src/components/resource/panels/AssessmentPanel.tsx +19 -15
  51. package/src/components/resource/panels/AssistSection.tsx +11 -13
  52. package/src/components/resource/panels/CollaborationPanel.tsx +29 -7
  53. package/src/components/resource/panels/CommentEntry.tsx +5 -4
  54. package/src/components/resource/panels/CommentsPanel.tsx +9 -11
  55. package/src/components/resource/panels/HighlightEntry.tsx +5 -4
  56. package/src/components/resource/panels/HighlightPanel.tsx +10 -11
  57. package/src/components/resource/panels/ReferenceEntry.tsx +8 -8
  58. package/src/components/resource/panels/ReferencesPanel.tsx +13 -12
  59. package/src/components/resource/panels/ResourceInfoPanel.tsx +7 -6
  60. package/src/components/resource/panels/TagEntry.tsx +5 -4
  61. package/src/components/resource/panels/TaggingPanel.tsx +10 -16
  62. package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +3 -2
  63. package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +18 -52
  64. package/src/components/resource/panels/__tests__/CollaborationPanel.test.tsx +51 -20
  65. package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +18 -56
  66. package/src/components/resource/panels/__tests__/HighlightPanel.annotationProgress.test.tsx +0 -1
  67. package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +4 -5
  68. package/src/components/resource/panels/__tests__/ReferencesPanel.observable-flow.test.tsx +153 -0
  69. package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +51 -106
  70. package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +15 -47
  71. package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +15 -47
  72. package/src/components/settings/SettingsPanel.tsx +8 -8
  73. package/src/components/settings/__tests__/SettingsPanel.test.tsx +12 -12
  74. package/src/features/admin-devops/components/AdminDevOpsPage.tsx +1 -1
  75. package/src/features/admin-exchange/components/AdminExchangePage.tsx +1 -1
  76. package/src/features/admin-exchange/components/ImportCard.tsx +2 -6
  77. package/src/features/admin-security/components/AdminSecurityPage.tsx +1 -1
  78. package/src/features/admin-users/components/AdminUsersPage.tsx +1 -1
  79. package/src/features/moderate-entity-tags/components/EntityTagsPage.tsx +1 -1
  80. package/src/features/moderate-recent/components/RecentDocumentsPage.tsx +1 -1
  81. package/src/features/moderate-tag-schemas/components/TagSchemasPage.tsx +1 -1
  82. package/src/features/moderation-linked-data/components/LinkedDataPage.tsx +1 -1
  83. package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +5 -3
  84. package/src/features/resource-compose/components/ResourceComposePage.tsx +5 -22
  85. package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +4 -3
  86. package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +1 -1
  87. package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +38 -45
  88. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +123 -192
  89. package/dist/KnowledgeBaseSessionContext-BNNunwzO.d.mts +0 -175
  90. package/dist/PdfAnnotationCanvas.client-CHDCGQBR.mjs.map +0 -1
  91. package/dist/chunk-OZICDVH7.mjs +0 -62
  92. package/dist/chunk-OZICDVH7.mjs.map +0 -1
  93. package/dist/chunk-R4CCMFJH.mjs +0 -877
  94. package/dist/chunk-R4CCMFJH.mjs.map +0 -1
  95. package/dist/chunk-VN5NY4SN.mjs +0 -200
  96. package/dist/chunk-VN5NY4SN.mjs.map +0 -1
  97. package/src/components/modals/ProposeEntitiesModal.tsx +0 -179
  98. package/src/components/modals/__tests__/ProposeEntitiesModal.test.tsx +0 -129
  99. package/src/features/resource-viewer/__tests__/AnnotationCreationPending.test.tsx +0 -323
  100. package/src/features/resource-viewer/__tests__/AnnotationDeletionIntegration.test.tsx +0 -245
  101. package/src/features/resource-viewer/__tests__/AnnotationProgressDismissal.test.tsx +0 -303
  102. package/src/features/resource-viewer/__tests__/BindFlowIntegration.test.tsx +0 -150
  103. package/src/features/resource-viewer/__tests__/DetectionFlowBug.test.tsx +0 -243
  104. package/src/features/resource-viewer/__tests__/DetectionFlowIntegration.test.tsx +0 -383
  105. package/src/features/resource-viewer/__tests__/ResourceMutations.test.tsx +0 -299
  106. package/src/features/resource-viewer/__tests__/ToastNotifications.test.tsx +0 -186
  107. package/src/features/resource-viewer/__tests__/YieldFlowIntegration.test.tsx +0 -429
  108. package/src/features/resource-viewer/__tests__/annotation-progress-flow.test.tsx +0 -348
@@ -1,186 +0,0 @@
1
- /**
2
- * Toast Notifications Test - Verifies Toast Integration
3
- *
4
- * This test verifies the fix for the issue identified in commit 9690806abc910bad490e684d6ef71d874a90579c.
5
- *
6
- * SOLUTION IMPLEMENTED:
7
- * - Pattern B: Both useMarkFlow and useYieldFlow call useToast internally
8
- * - Toast notifications are shown from within the hooks (self-contained)
9
- *
10
- * EXPECTED BEHAVIOR (after fix):
11
- * - Detection completes → User sees success toast ✓
12
- * - Detection fails → User sees error toast ✓
13
- * - Generation completes → User sees success toast ✓
14
- * - Generation fails → User sees error toast ✓
15
- */
16
-
17
- import { describe, it, expect, vi, beforeEach } from 'vitest';
18
- import { render, waitFor, act } from '@testing-library/react';
19
- import { EventBusProvider, useEventBus } from '../../../contexts/EventBusContext';
20
- import { ApiClientProvider } from '../../../contexts/ApiClientContext';
21
- import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
22
- import { resourceId } from '@semiont/core';
23
- import { useMarkFlow } from '../../../hooks/useMarkFlow';
24
- import { useYieldFlow } from '../../../hooks/useYieldFlow';
25
-
26
- // Mock the toast hook to track calls
27
- const mockShowSuccess = vi.fn();
28
- const mockShowError = vi.fn();
29
-
30
- vi.mock('../../../components/Toast', () => ({
31
- useToast: () => ({
32
- showSuccess: mockShowSuccess,
33
- showError: mockShowError,
34
- showInfo: vi.fn(),
35
- showWarning: vi.fn(),
36
- }),
37
- }));
38
-
39
- describe('Toast Notifications - Verifies Toast Integration', () => {
40
- let eventBusInstance: any;
41
- const rUri = resourceId('test');
42
-
43
- beforeEach(() => {
44
- mockShowSuccess.mockClear();
45
- mockShowError.mockClear();
46
- });
47
-
48
- /**
49
- * Test component that uses both hooks to verify toast integration
50
- */
51
- function TestComponentWithDetection() {
52
- eventBusInstance = useEventBus();
53
- useMarkFlow(rUri);
54
- return <div data-testid="test">Test</div>;
55
- }
56
-
57
- function TestComponentWithGeneration() {
58
- eventBusInstance = useEventBus();
59
- useYieldFlow('en', 'test-resource', vi.fn());
60
- return <div data-testid="test">Test</div>;
61
- }
62
-
63
- function renderDetectionTest() {
64
- return render(
65
- <EventBusProvider>
66
- <AuthTokenProvider token={null}>
67
- <ApiClientProvider baseUrl="http://localhost:4000">
68
- <TestComponentWithDetection />
69
- </ApiClientProvider>
70
- </AuthTokenProvider>
71
- </EventBusProvider>
72
- );
73
- }
74
-
75
- function renderGenerationTest() {
76
- return render(
77
- <EventBusProvider>
78
- <AuthTokenProvider token={null}>
79
- <ApiClientProvider baseUrl="http://localhost:4000">
80
- <TestComponentWithGeneration />
81
- </ApiClientProvider>
82
- </AuthTokenProvider>
83
- </EventBusProvider>
84
- );
85
- }
86
-
87
- describe('Detection Events Trigger Toasts', () => {
88
- it('mark:assist-finished shows success toast', async () => {
89
- renderDetectionTest();
90
-
91
- // Clear any potential mount-related calls
92
- await new Promise(resolve => setTimeout(resolve, 100));
93
- mockShowSuccess.mockClear();
94
-
95
- // Emit detection finished event (what SSE would emit)
96
- act(() => {
97
- eventBusInstance.get('mark:assist-finished').next({
98
- motivation: 'linking' as any
99
- });
100
- });
101
-
102
- // Wait for toast to be called
103
- await waitFor(() => {
104
- expect(mockShowSuccess).toHaveBeenCalledWith('Annotation complete');
105
- });
106
- });
107
-
108
- it('mark:assist-failed shows error toast', async () => {
109
- renderDetectionTest();
110
-
111
- await new Promise(resolve => setTimeout(resolve, 100));
112
- mockShowError.mockClear();
113
-
114
- // Emit detection failed event
115
- act(() => {
116
- eventBusInstance.get('mark:assist-failed').next({
117
- resourceId: 'test' as any,
118
- message: 'AI service unavailable',
119
- });
120
- });
121
-
122
- // Wait for toast to be called
123
- await waitFor(() => {
124
- expect(mockShowError).toHaveBeenCalledWith('AI service unavailable');
125
- });
126
- });
127
- });
128
-
129
- describe('Generation Events Trigger Toasts', () => {
130
- it('yield:finished shows success toast', async () => {
131
- renderGenerationTest();
132
-
133
- await new Promise(resolve => setTimeout(resolve, 100));
134
- mockShowSuccess.mockClear();
135
-
136
- // Emit generation finished event
137
- act(() => {
138
- eventBusInstance.get('yield:finished').next({
139
- status: 'complete',
140
- message: 'Document generated successfully',
141
- percentage: 100,
142
- referenceId: 'ref-1',
143
- });
144
- });
145
-
146
- // Wait for toast to be called
147
- await waitFor(() => {
148
- expect(mockShowSuccess).toHaveBeenCalledWith('Resource created successfully!');
149
- });
150
- });
151
-
152
- it('yield:failed shows error toast', async () => {
153
- renderGenerationTest();
154
-
155
- await new Promise(resolve => setTimeout(resolve, 100));
156
- mockShowError.mockClear();
157
-
158
- // Emit generation failed event
159
- act(() => {
160
- eventBusInstance.get('yield:failed').next({
161
- error: 'Failed to generate document',
162
- });
163
- });
164
-
165
- // Wait for toast to be called
166
- await waitFor(() => {
167
- expect(mockShowError).toHaveBeenCalledWith('Resource generation failed: Failed to generate document');
168
- });
169
- });
170
- });
171
-
172
- describe('Verification: Toast Mock Works Correctly', () => {
173
- it('SANITY CHECK: can verify toast is NOT called', () => {
174
- // This test proves our mock works correctly
175
- expect(mockShowSuccess).not.toHaveBeenCalled();
176
- expect(mockShowError).not.toHaveBeenCalled();
177
-
178
- // If we call them, they should be tracked
179
- mockShowSuccess('test');
180
- mockShowError('test');
181
-
182
- expect(mockShowSuccess).toHaveBeenCalledWith('test');
183
- expect(mockShowError).toHaveBeenCalledWith('test');
184
- });
185
- });
186
- });
@@ -1,429 +0,0 @@
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
- * - useYieldFlow (REAL, with inlined progress state)
8
- * - useBindFlow (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 { useYieldFlow } from '../../../hooks/useYieldFlow';
25
- import { EventBusProvider, useEventBus } from '../../../contexts/EventBusContext';
26
- import { ApiClientProvider } from '../../../contexts/ApiClientContext';
27
- import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
28
- import { useBindFlow } from '../../../hooks/useBindFlow';
29
- import { SemiontApiClient } from '@semiont/api-client';
30
- import type { AnnotationId, ResourceId } from '@semiont/core';
31
- import { resourceId, annotationId } from '@semiont/core';
32
- import type { EventMap } from '@semiont/core';
33
-
34
- // Mock Toast module to prevent "useToast must be used within a ToastProvider" errors
35
- vi.mock('../../../components/Toast', () => ({
36
- useToast: () => ({
37
- showSuccess: vi.fn(),
38
- showError: vi.fn(),
39
- showInfo: vi.fn(),
40
- showWarning: vi.fn(),
41
- }),
42
- }));
43
-
44
- describe('Generation Flow - Feature Integration', () => {
45
- let generateResourceSpy: any;
46
- let mockShowSuccess: ReturnType<typeof vi.fn>;
47
- let mockShowError: ReturnType<typeof vi.fn>;
48
- let mockCacheManager: { invalidate: ReturnType<typeof vi.fn> };
49
-
50
- beforeEach(() => {
51
- vi.clearAllMocks();
52
-
53
- // Spy on SemiontApiClient prototype HTTP method (namespace methods call this)
54
- generateResourceSpy = vi.spyOn(SemiontApiClient.prototype, 'yieldResourceFromAnnotation').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
55
-
56
- // Mock callbacks
57
- mockShowSuccess = vi.fn();
58
- mockShowError = vi.fn();
59
- mockCacheManager = { invalidate: vi.fn() };
60
- });
61
-
62
- afterEach(() => {
63
- vi.restoreAllMocks();
64
- });
65
-
66
- it('should call yieldResource exactly ONCE when generation starts', async () => {
67
- const testResourceId = resourceId('test-resource');
68
- const testAnnotationId = annotationId('test-annotation');
69
-
70
- const { emitGenerationStart, getEventBus } = renderYieldFlow(
71
- testResourceId
72
- );
73
-
74
- // Trigger generation with full options
75
- act(() => {
76
- emitGenerationStart(testAnnotationId, testResourceId, {
77
- title: 'Generated Document',
78
- prompt: 'Create a comprehensive document',
79
- language: 'en',
80
- temperature: 0.7,
81
- maxTokens: 2000,
82
- context: {
83
- sourceText: 'Reference text from the document',
84
- entityTypes: ['Person', 'Organization'],
85
- },
86
- });
87
- });
88
-
89
- // CRITICAL ASSERTION: API called exactly once (not twice!)
90
- await waitFor(() => {
91
- expect(generateResourceSpy).toHaveBeenCalledTimes(1);
92
- });
93
-
94
- // Verify correct parameters — resourceId and annotationId are now bare IDs
95
- expect(generateResourceSpy).toHaveBeenCalledWith(
96
- testResourceId,
97
- testAnnotationId,
98
- expect.objectContaining({
99
- title: 'Generated Document',
100
- prompt: 'Create a comprehensive document',
101
- language: 'en',
102
- temperature: 0.7,
103
- maxTokens: 2000,
104
- }),
105
- expect.objectContaining({ auth: undefined })
106
- );
107
- });
108
-
109
- it('should propagate SSE progress events to useYieldProgress state', async () => {
110
- const testResourceId = resourceId('test-resource');
111
- const testAnnotationId = annotationId('test-annotation');
112
-
113
- const { emitGenerationStart, getEventBus } = renderYieldFlow(
114
- testResourceId
115
- );
116
-
117
- // Start generation
118
- act(() => {
119
- emitGenerationStart(testAnnotationId, testResourceId, {
120
- title: 'Test Doc',
121
- context: { sourceText: 'test' },
122
- });
123
- });
124
-
125
- // Wait for stream to be created
126
- await waitFor(() => {
127
- expect(generateResourceSpy).toHaveBeenCalled();
128
- });
129
-
130
- // Simulate SSE progress callback being invoked
131
- act(() => {
132
- getEventBus().get('yield:progress').next({
133
- status: 'generating',
134
- message: 'Generating content...',
135
- percentage: 25,
136
- });
137
- });
138
-
139
- // Verify progress propagated to UI
140
- await waitFor(() => {
141
- expect(screen.getByTestId('progress')).toHaveTextContent('Generating content...');
142
- expect(screen.getByTestId('is-generating')).toHaveTextContent('true');
143
- });
144
- });
145
-
146
- it('should handle multiple progress updates correctly', async () => {
147
- const testResourceId = resourceId('test-resource');
148
- const testAnnotationId = annotationId('test-annotation');
149
-
150
- const { emitGenerationStart, getEventBus } = renderYieldFlow(
151
- testResourceId
152
- );
153
-
154
- // Start generation
155
- act(() => {
156
- emitGenerationStart(testAnnotationId, testResourceId, {
157
- title: 'Test',
158
- context: { sourceText: 'test' },
159
- });
160
- });
161
-
162
- await waitFor(() => {
163
- expect(generateResourceSpy).toHaveBeenCalledTimes(1);
164
- });
165
-
166
- // First progress update
167
- act(() => {
168
- getEventBus().get('yield:progress').next({
169
- status: 'started',
170
- message: 'Starting generation...',
171
- percentage: 0,
172
- });
173
- });
174
-
175
- await waitFor(() => {
176
- expect(screen.getByTestId('progress')).toHaveTextContent('Starting generation...');
177
- });
178
-
179
- // Second progress update
180
- act(() => {
181
- getEventBus().get('yield:progress').next({
182
- status: 'generating',
183
- message: 'Creating document structure...',
184
- percentage: 50,
185
- });
186
- });
187
-
188
- await waitFor(() => {
189
- expect(screen.getByTestId('progress')).toHaveTextContent('Creating document structure...');
190
- });
191
-
192
- // Final progress update via onComplete
193
- act(() => {
194
- getEventBus().get('yield:finished').next({
195
- status: 'complete',
196
- referenceId: testAnnotationId,
197
- message: 'Document created successfully',
198
- percentage: 100,
199
- resourceName: 'Generated Document',
200
- });
201
- });
202
-
203
- await waitFor(() => {
204
- expect(screen.getByTestId('progress')).toHaveTextContent('Document created successfully');
205
- // Progress stays visible after completion (like detection flow)
206
- });
207
- });
208
-
209
- it('should show success toast on generation complete', async () => {
210
- const testResourceId = resourceId('test-resource');
211
- const testAnnotationId = annotationId('test-annotation');
212
-
213
- const { emitGenerationStart, getEventBus } = renderYieldFlow(
214
- testResourceId
215
- );
216
-
217
- // Start generation
218
- act(() => {
219
- emitGenerationStart(testAnnotationId, testResourceId, {
220
- title: 'Test',
221
- context: { sourceText: 'test' },
222
- });
223
- });
224
-
225
- await waitFor(() => {
226
- expect(generateResourceSpy).toHaveBeenCalled();
227
- });
228
-
229
- // Simulate completion with final chunk
230
- act(() => {
231
- getEventBus().get('yield:progress').next({
232
- status: 'complete',
233
- message: 'Complete',
234
- resourceName: 'Generated Document',
235
- });
236
- });
237
-
238
- // Emit completion event
239
- act(() => {
240
- getEventBus().get('yield:finished').next({
241
- status: 'complete',
242
- referenceId: testAnnotationId,
243
- resourceName: 'Generated Document',
244
- percentage: 100,
245
- });
246
- });
247
-
248
- // Verify generation completes successfully
249
- // Note: Progress stays visible after completion (like detection flow)
250
- });
251
-
252
- it('should clear progress on generation failure', async () => {
253
- const testResourceId = resourceId('test-resource');
254
- const testAnnotationId = annotationId('test-annotation');
255
-
256
- const { emitGenerationStart, getEventBus } = renderYieldFlow(
257
- testResourceId
258
- );
259
-
260
- // Start generation
261
- act(() => {
262
- emitGenerationStart(testAnnotationId, testResourceId, {
263
- title: 'Test',
264
- context: { sourceText: 'test' },
265
- });
266
- });
267
-
268
- // Add some progress
269
- act(() => {
270
- getEventBus().get('yield:progress').next({
271
- status: 'generating',
272
- message: 'Generating...',
273
- });
274
- });
275
-
276
- await waitFor(() => {
277
- expect(screen.getByTestId('progress')).toHaveTextContent('Generating...');
278
- });
279
-
280
- // Emit failure
281
- act(() => {
282
- getEventBus().get('yield:failed').next({ error: 'Network error' });
283
- });
284
-
285
- // Verify: progress cleared and not generating
286
- await waitFor(() => {
287
- expect(screen.getByTestId('is-generating')).toHaveTextContent('false');
288
- expect(screen.getByTestId('progress')).toHaveTextContent('No progress');
289
- });
290
- });
291
-
292
- it('should only call API once even with multiple renders', async () => {
293
- const testResourceId = resourceId('test-resource');
294
- const testAnnotationId = annotationId('test-annotation');
295
-
296
- const { emitGenerationStart } = renderYieldFlow(
297
- testResourceId
298
- );
299
-
300
- // Trigger generation
301
- act(() => {
302
- emitGenerationStart(testAnnotationId, testResourceId, {
303
- title: 'Test',
304
- context: { sourceText: 'test' },
305
- });
306
- });
307
-
308
- // Wait for operation to complete
309
- await waitFor(() => {
310
- expect(generateResourceSpy).toHaveBeenCalled();
311
- });
312
-
313
- // VERIFY: API called exactly once
314
- expect(generateResourceSpy).toHaveBeenCalledTimes(1);
315
- });
316
-
317
- it('should forward final chunk as progress before emitting complete', async () => {
318
- const testResourceId = resourceId('test-resource');
319
- const testAnnotationId = annotationId('test-annotation');
320
-
321
- const { emitGenerationStart, getEventBus } = renderYieldFlow(
322
- testResourceId
323
- );
324
-
325
- // Start generation
326
- act(() => {
327
- emitGenerationStart(testAnnotationId, testResourceId, {
328
- title: 'Test',
329
- context: { sourceText: 'test' },
330
- });
331
- });
332
-
333
- await waitFor(() => {
334
- expect(generateResourceSpy).toHaveBeenCalled();
335
- });
336
-
337
- // Simulate onComplete with final chunk
338
- act(() => {
339
- getEventBus().get('yield:finished').next({
340
- status: 'complete',
341
- referenceId: testAnnotationId,
342
- message: 'Document created: My Document',
343
- resourceName: 'My Document',
344
- percentage: 100,
345
- });
346
- });
347
-
348
- // Verify final chunk is visible as progress
349
- await waitFor(() => {
350
- expect(screen.getByTestId('progress')).toHaveTextContent('Document created: My Document');
351
- // Progress stays visible after completion (like detection flow)
352
- });
353
- });
354
- });
355
-
356
- /**
357
- * Helper: Render useYieldFlow hook with real component composition
358
- * Returns methods to interact with the rendered component
359
- */
360
- function renderYieldFlow(
361
- testResourceId: ResourceId
362
- ) {
363
- let eventBusInstance: ReturnType<typeof useEventBus>;
364
- let generateFn: ReturnType<typeof useYieldFlow>['onGenerateDocument'];
365
-
366
- // Component to capture EventBus instance and set up event operations
367
- function EventBusCapture() {
368
- eventBusInstance = useEventBus();
369
- useBindFlow(testResourceId);
370
- return null;
371
- }
372
-
373
- // Test harness component that uses the hook
374
- function YieldFlowTestHarness() {
375
- const {
376
- isGenerating,
377
- generationProgress,
378
- onGenerateDocument,
379
- } = useYieldFlow(
380
- 'en',
381
- testResourceId,
382
- vi.fn()
383
- );
384
-
385
- generateFn = onGenerateDocument;
386
-
387
- return (
388
- <div>
389
- <div data-testid="is-generating">
390
- {isGenerating ? 'true' : 'false'}
391
- </div>
392
- <div data-testid="progress">
393
- {generationProgress?.message || 'No progress'}
394
- </div>
395
- </div>
396
- );
397
- }
398
-
399
- render(
400
- <EventBusProvider>
401
- <AuthTokenProvider token={null}>
402
- <ApiClientProvider baseUrl="http://localhost:4000">
403
- <EventBusCapture />
404
- <YieldFlowTestHarness />
405
- </ApiClientProvider>
406
- </AuthTokenProvider>
407
- </EventBusProvider>
408
- );
409
-
410
- return {
411
- emitGenerationStart: (
412
- aId: AnnotationId,
413
- _rId: ResourceId,
414
- options: {
415
- title: string;
416
- storageUri?: string;
417
- prompt?: string;
418
- language?: string;
419
- temperature?: number;
420
- maxTokens?: number;
421
- context: any;
422
- }
423
- ) => {
424
- // Call the hook's callback directly (no longer EventBus-driven)
425
- generateFn(aId as string, { storageUri: options.storageUri ?? 'file:///tmp/test', ...options });
426
- },
427
- getEventBus: () => eventBusInstance,
428
- };
429
- }