@semiont/react-ui 0.2.33 → 0.2.34-build.89
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.
- package/dist/EventBusContext-BmzEcGHZ.d.mts +177 -0
- package/dist/{PdfAnnotationCanvas.client-FGV33CWN.mjs → PdfAnnotationCanvas.client-VLNA5O5M.mjs} +7 -7
- package/dist/PdfAnnotationCanvas.client-VLNA5O5M.mjs.map +1 -0
- package/dist/{chunk-YPYLOBA2.mjs → chunk-C63BARI7.mjs} +3 -2
- package/dist/chunk-C63BARI7.mjs.map +1 -0
- package/dist/{chunk-FC6SGLLT.mjs → chunk-M7SZRRIE.mjs} +24 -16
- package/dist/chunk-M7SZRRIE.mjs.map +1 -0
- package/dist/chunk-ULIET3MW.mjs +31 -0
- package/dist/chunk-ULIET3MW.mjs.map +1 -0
- package/dist/index.d.mts +33 -60
- package/dist/index.mjs +171 -363
- package/dist/index.mjs.map +1 -1
- package/dist/test-utils.d.mts +3 -5
- package/dist/test-utils.mjs +2 -2
- package/dist/test-utils.mjs.map +1 -1
- package/package.json +2 -3
- package/src/components/CodeMirrorRenderer.tsx +4 -4
- package/src/components/DetectionProgressWidget.tsx +3 -3
- package/src/components/LiveRegion.tsx +1 -1
- package/src/components/Toolbar.tsx +1 -1
- package/src/components/annotation/AnnotateToolbar.tsx +5 -5
- package/src/components/annotation/__tests__/AnnotateToolbar.test.tsx +4 -4
- package/src/components/annotation-popups/JsonLdView.tsx +1 -1
- package/src/components/image-annotation/AnnotationOverlay.tsx +9 -9
- package/src/components/image-annotation/SvgDrawingCanvas.tsx +5 -5
- package/src/components/navigation/CollapsibleResourceNavigation.tsx +4 -4
- package/src/components/navigation/ObservableLink.tsx +1 -1
- package/src/components/navigation/SimpleNavigation.tsx +1 -1
- package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +6 -6
- package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +2 -2
- package/src/components/resource/AnnotateView.tsx +5 -4
- package/src/components/resource/AnnotationHistory.tsx +1 -1
- package/src/components/resource/BrowseView.tsx +5 -4
- package/src/components/resource/ResourceViewer.tsx +5 -4
- package/src/components/resource/__tests__/BrowseView.test.tsx +11 -22
- package/src/components/resource/__tests__/ResourceViewer.mode-switch.test.tsx +1 -1
- package/src/components/resource/event-formatting.ts +1 -1
- package/src/components/resource/panels/AssessmentEntry.tsx +2 -2
- package/src/components/resource/panels/AssessmentPanel.tsx +4 -4
- package/src/components/resource/panels/CommentEntry.tsx +2 -2
- package/src/components/resource/panels/CommentsPanel.tsx +4 -4
- package/src/components/resource/panels/DetectSection.tsx +3 -3
- package/src/components/resource/panels/HighlightEntry.tsx +2 -2
- package/src/components/resource/panels/HighlightPanel.tsx +2 -2
- package/src/components/resource/panels/JsonLdPanel.tsx +1 -1
- package/src/components/resource/panels/ReferenceEntry.tsx +6 -6
- package/src/components/resource/panels/ReferencesPanel.tsx +5 -5
- package/src/components/resource/panels/ResourceInfoPanel.tsx +3 -3
- package/src/components/resource/panels/StatisticsPanel.tsx +1 -1
- package/src/components/resource/panels/TagEntry.tsx +2 -2
- package/src/components/resource/panels/TaggingPanel.tsx +5 -5
- package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +1 -1
- package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +5 -5
- package/src/components/resource/panels/__tests__/CommentEntry.test.tsx +10 -10
- package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +5 -5
- package/src/components/resource/panels/__tests__/DetectSection.test.tsx +9 -9
- package/src/components/resource/panels/__tests__/HighlightPanel.detectionProgress.test.tsx +1 -1
- package/src/components/resource/panels/__tests__/JsonLdPanel.test.tsx +1 -1
- package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +4 -4
- package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +4 -4
- package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +5 -5
- package/src/components/settings/SettingsPanel.tsx +5 -5
- package/src/components/viewers/ImageViewer.tsx +1 -1
- package/src/features/resource-compose/components/ResourceComposePage.tsx +1 -1
- package/src/features/resource-discovery/components/ResourceCard.tsx +1 -1
- package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +1 -1
- package/src/features/resource-viewer/__tests__/AnnotationCreationPending.test.tsx +7 -5
- package/src/features/resource-viewer/__tests__/AnnotationDeletionIntegration.test.tsx +5 -4
- package/src/features/resource-viewer/__tests__/DetectionFlowBug.test.tsx +5 -5
- package/src/features/resource-viewer/__tests__/DetectionFlowIntegration.test.tsx +29 -43
- package/src/features/resource-viewer/__tests__/DetectionProgressDismissal.test.tsx +20 -39
- package/src/features/resource-viewer/__tests__/GenerationFlowIntegration.test.tsx +38 -46
- package/src/features/resource-viewer/__tests__/ResolutionFlowIntegration.test.tsx +36 -43
- package/src/features/resource-viewer/__tests__/ResourceMutations.test.tsx +8 -8
- package/src/features/resource-viewer/__tests__/detection-progress-flow.test.tsx +14 -21
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +8 -7
- package/dist/EventBusContext-CJjL_cCf.d.mts +0 -462
- package/dist/PdfAnnotationCanvas.client-FGV33CWN.mjs.map +0 -1
- package/dist/chunk-FC6SGLLT.mjs.map +0 -1
- package/dist/chunk-XS27QKGP.mjs +0 -55
- package/dist/chunk-XS27QKGP.mjs.map +0 -1
- package/dist/chunk-YPYLOBA2.mjs.map +0 -1
|
@@ -27,32 +27,16 @@ import { ApiClientProvider } from '../../../contexts/ApiClientContext';
|
|
|
27
27
|
import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
|
|
28
28
|
import { useResolutionFlow } from '../../../hooks/useResolutionFlow';
|
|
29
29
|
import { SSEClient } from '@semiont/api-client';
|
|
30
|
-
import type { ResourceUri, AnnotationUri } from '@semiont/
|
|
31
|
-
import { resourceUri, annotationUri } from '@semiont/
|
|
30
|
+
import type { ResourceUri, AnnotationUri } from '@semiont/core';
|
|
31
|
+
import { resourceUri, annotationUri } from '@semiont/core';
|
|
32
32
|
import type { Emitter } from 'mitt';
|
|
33
|
-
import type { EventMap } from '
|
|
33
|
+
import type { EventMap } from '@semiont/core';
|
|
34
34
|
|
|
35
|
-
// Mock SSE stream
|
|
35
|
+
// Mock SSE stream - SSE now emits directly to EventBus, no callbacks
|
|
36
36
|
const createMockGenerationStream = () => {
|
|
37
|
-
|
|
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
|
-
}),
|
|
37
|
+
return {
|
|
53
38
|
close: vi.fn(),
|
|
54
39
|
};
|
|
55
|
-
return stream;
|
|
56
40
|
};
|
|
57
41
|
|
|
58
42
|
describe('Generation Flow - Feature Integration', () => {
|
|
@@ -107,7 +91,7 @@ describe('Generation Flow - Feature Integration', () => {
|
|
|
107
91
|
const testResourceUri = resourceUri('http://localhost:4000/resources/test-resource');
|
|
108
92
|
const testAnnotationUri = annotationUri('http://localhost:4000/resources/test-resource/annotations/test-annotation');
|
|
109
93
|
|
|
110
|
-
const { emitGenerationStart } = renderGenerationFlow(
|
|
94
|
+
const { emitGenerationStart, getEventBus } = renderGenerationFlow(
|
|
111
95
|
testResourceUri
|
|
112
96
|
);
|
|
113
97
|
|
|
@@ -146,7 +130,7 @@ describe('Generation Flow - Feature Integration', () => {
|
|
|
146
130
|
entityTypes: ['Person', 'Organization'],
|
|
147
131
|
},
|
|
148
132
|
},
|
|
149
|
-
{ auth: undefined }
|
|
133
|
+
expect.objectContaining({ auth: undefined })
|
|
150
134
|
);
|
|
151
135
|
});
|
|
152
136
|
|
|
@@ -154,7 +138,7 @@ describe('Generation Flow - Feature Integration', () => {
|
|
|
154
138
|
const testResourceUri = resourceUri('http://localhost:4000/resources/test-resource');
|
|
155
139
|
const testAnnotationUri = annotationUri('http://localhost:4000/resources/test-resource/annotations/test-annotation');
|
|
156
140
|
|
|
157
|
-
const { emitGenerationStart } = renderGenerationFlow(
|
|
141
|
+
const { emitGenerationStart, getEventBus } = renderGenerationFlow(
|
|
158
142
|
testResourceUri
|
|
159
143
|
);
|
|
160
144
|
|
|
@@ -173,7 +157,7 @@ describe('Generation Flow - Feature Integration', () => {
|
|
|
173
157
|
|
|
174
158
|
// Simulate SSE progress callback being invoked
|
|
175
159
|
act(() => {
|
|
176
|
-
|
|
160
|
+
getEventBus().get('generation:progress').next({
|
|
177
161
|
status: 'generating',
|
|
178
162
|
message: 'Generating content...',
|
|
179
163
|
percentage: 25,
|
|
@@ -191,7 +175,7 @@ describe('Generation Flow - Feature Integration', () => {
|
|
|
191
175
|
const testResourceUri = resourceUri('http://localhost:4000/resources/test-resource');
|
|
192
176
|
const testAnnotationUri = annotationUri('http://localhost:4000/resources/test-resource/annotations/test-annotation');
|
|
193
177
|
|
|
194
|
-
const { emitGenerationStart } = renderGenerationFlow(
|
|
178
|
+
const { emitGenerationStart, getEventBus } = renderGenerationFlow(
|
|
195
179
|
testResourceUri
|
|
196
180
|
);
|
|
197
181
|
|
|
@@ -209,7 +193,7 @@ describe('Generation Flow - Feature Integration', () => {
|
|
|
209
193
|
|
|
210
194
|
// First progress update
|
|
211
195
|
act(() => {
|
|
212
|
-
|
|
196
|
+
getEventBus().get('generation:progress').next({
|
|
213
197
|
status: 'started',
|
|
214
198
|
message: 'Starting generation...',
|
|
215
199
|
percentage: 0,
|
|
@@ -222,7 +206,7 @@ describe('Generation Flow - Feature Integration', () => {
|
|
|
222
206
|
|
|
223
207
|
// Second progress update
|
|
224
208
|
act(() => {
|
|
225
|
-
|
|
209
|
+
getEventBus().get('generation:progress').next({
|
|
226
210
|
status: 'generating',
|
|
227
211
|
message: 'Creating document structure...',
|
|
228
212
|
percentage: 50,
|
|
@@ -235,11 +219,14 @@ describe('Generation Flow - Feature Integration', () => {
|
|
|
235
219
|
|
|
236
220
|
// Final progress update via onComplete
|
|
237
221
|
act(() => {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
222
|
+
getEventBus().get('generation:complete').next({
|
|
223
|
+
annotationUri: testAnnotationUri,
|
|
224
|
+
progress: {
|
|
225
|
+
status: 'complete',
|
|
226
|
+
message: 'Document created successfully',
|
|
227
|
+
percentage: 100,
|
|
228
|
+
resourceName: 'Generated Document',
|
|
229
|
+
}
|
|
243
230
|
});
|
|
244
231
|
});
|
|
245
232
|
|
|
@@ -271,7 +258,7 @@ describe('Generation Flow - Feature Integration', () => {
|
|
|
271
258
|
|
|
272
259
|
// Simulate completion with final chunk
|
|
273
260
|
act(() => {
|
|
274
|
-
|
|
261
|
+
getEventBus().get('generation:progress').next({
|
|
275
262
|
status: 'complete',
|
|
276
263
|
message: 'Complete',
|
|
277
264
|
resourceName: 'Generated Document',
|
|
@@ -280,7 +267,7 @@ describe('Generation Flow - Feature Integration', () => {
|
|
|
280
267
|
|
|
281
268
|
// Emit completion event
|
|
282
269
|
act(() => {
|
|
283
|
-
getEventBus().
|
|
270
|
+
getEventBus().get('generation:complete').next({
|
|
284
271
|
annotationUri: testAnnotationUri,
|
|
285
272
|
progress: {
|
|
286
273
|
status: 'complete',
|
|
@@ -311,7 +298,7 @@ describe('Generation Flow - Feature Integration', () => {
|
|
|
311
298
|
|
|
312
299
|
// Add some progress
|
|
313
300
|
act(() => {
|
|
314
|
-
|
|
301
|
+
getEventBus().get('generation:progress').next({
|
|
315
302
|
status: 'generating',
|
|
316
303
|
message: 'Generating...',
|
|
317
304
|
});
|
|
@@ -323,7 +310,7 @@ describe('Generation Flow - Feature Integration', () => {
|
|
|
323
310
|
|
|
324
311
|
// Emit failure
|
|
325
312
|
act(() => {
|
|
326
|
-
getEventBus().
|
|
313
|
+
getEventBus().get('generation:failed').next({ error: new Error('Network error') });
|
|
327
314
|
});
|
|
328
315
|
|
|
329
316
|
// Verify: progress cleared and not generating
|
|
@@ -343,7 +330,7 @@ describe('Generation Flow - Feature Integration', () => {
|
|
|
343
330
|
|
|
344
331
|
// Add an additional event listener (simulating multiple subscribers)
|
|
345
332
|
const additionalListener = vi.fn();
|
|
346
|
-
getEventBus().
|
|
333
|
+
const subscription = getEventBus().get('generation:start').subscribe(additionalListener);
|
|
347
334
|
|
|
348
335
|
// Trigger generation
|
|
349
336
|
act(() => {
|
|
@@ -363,13 +350,15 @@ describe('Generation Flow - Feature Integration', () => {
|
|
|
363
350
|
|
|
364
351
|
// VERIFY: Our additional listener was called (events work)
|
|
365
352
|
expect(additionalListener).toHaveBeenCalledTimes(1);
|
|
353
|
+
|
|
354
|
+
subscription.unsubscribe();
|
|
366
355
|
});
|
|
367
356
|
|
|
368
357
|
it('should forward final chunk as progress before emitting complete', async () => {
|
|
369
358
|
const testResourceUri = resourceUri('http://localhost:4000/resources/test-resource');
|
|
370
359
|
const testAnnotationUri = annotationUri('http://localhost:4000/resources/test-resource/annotations/test-annotation');
|
|
371
360
|
|
|
372
|
-
const { emitGenerationStart } = renderGenerationFlow(
|
|
361
|
+
const { emitGenerationStart, getEventBus } = renderGenerationFlow(
|
|
373
362
|
testResourceUri
|
|
374
363
|
);
|
|
375
364
|
|
|
@@ -387,11 +376,14 @@ describe('Generation Flow - Feature Integration', () => {
|
|
|
387
376
|
|
|
388
377
|
// Simulate onComplete with final chunk
|
|
389
378
|
act(() => {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
379
|
+
getEventBus().get('generation:complete').next({
|
|
380
|
+
annotationUri: testAnnotationUri,
|
|
381
|
+
progress: {
|
|
382
|
+
status: 'complete',
|
|
383
|
+
message: 'Document created: My Document',
|
|
384
|
+
resourceName: 'My Document',
|
|
385
|
+
percentage: 100,
|
|
386
|
+
}
|
|
395
387
|
});
|
|
396
388
|
});
|
|
397
389
|
|
|
@@ -470,7 +462,7 @@ function renderGenerationFlow(
|
|
|
470
462
|
resourceUri: ResourceUri,
|
|
471
463
|
defaultTitle: string
|
|
472
464
|
) => {
|
|
473
|
-
eventBusInstance.
|
|
465
|
+
eventBusInstance.get('generation:modal-open').next({
|
|
474
466
|
annotationUri,
|
|
475
467
|
resourceUri,
|
|
476
468
|
defaultTitle,
|
|
@@ -488,7 +480,7 @@ function renderGenerationFlow(
|
|
|
488
480
|
context: any;
|
|
489
481
|
}
|
|
490
482
|
) => {
|
|
491
|
-
eventBusInstance.
|
|
483
|
+
eventBusInstance.get('generation:start').next({
|
|
492
484
|
annotationUri,
|
|
493
485
|
resourceUri,
|
|
494
486
|
options,
|
|
@@ -22,7 +22,8 @@ import { useResolutionFlow } from '../../../hooks/useResolutionFlow';
|
|
|
22
22
|
import { EventBusProvider, useEventBus, resetEventBusForTesting } from '../../../contexts/EventBusContext';
|
|
23
23
|
import { ApiClientProvider } from '../../../contexts/ApiClientContext';
|
|
24
24
|
import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
|
|
25
|
-
import { SemiontApiClient
|
|
25
|
+
import { SemiontApiClient } from '@semiont/api-client';
|
|
26
|
+
import { resourceUri, accessToken } from '@semiont/core';
|
|
26
27
|
|
|
27
28
|
describe('Resolution Flow - Search Modal & Body Update Integration', () => {
|
|
28
29
|
let updateAnnotationBodySpy: ReturnType<typeof vi.fn>;
|
|
@@ -66,15 +67,7 @@ describe('Resolution Flow - Search Modal & Body Update Integration', () => {
|
|
|
66
67
|
|
|
67
68
|
return {
|
|
68
69
|
getState: () => lastState!,
|
|
69
|
-
|
|
70
|
-
act(() => { eventBusInstance!.emit(event as any, payload as any); });
|
|
71
|
-
},
|
|
72
|
-
on: (event: Parameters<typeof eventBusInstance.on>[0], handler: (payload: any) => void) => {
|
|
73
|
-
eventBusInstance!.on(event as any, handler);
|
|
74
|
-
},
|
|
75
|
-
off: (event: Parameters<typeof eventBusInstance.off>[0], handler: (payload: any) => void) => {
|
|
76
|
-
eventBusInstance!.off(event as any, handler);
|
|
77
|
-
},
|
|
70
|
+
getEventBus: () => eventBusInstance!,
|
|
78
71
|
};
|
|
79
72
|
}
|
|
80
73
|
|
|
@@ -89,12 +82,12 @@ describe('Resolution Flow - Search Modal & Body Update Integration', () => {
|
|
|
89
82
|
// ─── reference:link ─────────────────────────────────────────────────────────
|
|
90
83
|
|
|
91
84
|
it('reference:link emits resolution:search-requested with referenceId and searchTerm', () => {
|
|
92
|
-
const {
|
|
85
|
+
const { getEventBus } = renderResolutionFlow();
|
|
93
86
|
const searchRequestedSpy = vi.fn();
|
|
94
87
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
88
|
+
const subscription = getEventBus().get('resolution:search-requested').subscribe(searchRequestedSpy);
|
|
89
|
+
act(() => { getEventBus().get('reference:link').next({ annotationUri: 'ann-uri-123', searchTerm: 'climate change' }); });
|
|
90
|
+
subscription.unsubscribe();
|
|
98
91
|
|
|
99
92
|
expect(searchRequestedSpy).toHaveBeenCalledTimes(1);
|
|
100
93
|
expect(searchRequestedSpy).toHaveBeenCalledWith({
|
|
@@ -106,11 +99,11 @@ describe('Resolution Flow - Search Modal & Body Update Integration', () => {
|
|
|
106
99
|
// ─── resolution:search-requested ────────────────────────────────────────────
|
|
107
100
|
|
|
108
101
|
it('resolution:search-requested opens the search modal', async () => {
|
|
109
|
-
const { getState,
|
|
102
|
+
const { getState, getEventBus } = renderResolutionFlow();
|
|
110
103
|
|
|
111
104
|
expect(getState().searchModalOpen).toBe(false);
|
|
112
105
|
|
|
113
|
-
|
|
106
|
+
act(() => { getEventBus().get('resolution:search-requested').next({ referenceId: 'ref-abc', searchTerm: 'oceans' }); });
|
|
114
107
|
|
|
115
108
|
await waitFor(() => {
|
|
116
109
|
expect(getState().searchModalOpen).toBe(true);
|
|
@@ -118,9 +111,9 @@ describe('Resolution Flow - Search Modal & Body Update Integration', () => {
|
|
|
118
111
|
});
|
|
119
112
|
|
|
120
113
|
it('resolution:search-requested sets pendingReferenceId', async () => {
|
|
121
|
-
const { getState,
|
|
114
|
+
const { getState, getEventBus } = renderResolutionFlow();
|
|
122
115
|
|
|
123
|
-
|
|
116
|
+
act(() => { getEventBus().get('resolution:search-requested').next({ referenceId: 'ref-xyz', searchTerm: 'forests' }); });
|
|
124
117
|
|
|
125
118
|
await waitFor(() => {
|
|
126
119
|
expect(getState().pendingReferenceId).toBe('ref-xyz');
|
|
@@ -128,10 +121,10 @@ describe('Resolution Flow - Search Modal & Body Update Integration', () => {
|
|
|
128
121
|
});
|
|
129
122
|
|
|
130
123
|
it('reference:link → resolution:search-requested chain opens modal end-to-end', async () => {
|
|
131
|
-
const { getState,
|
|
124
|
+
const { getState, getEventBus } = renderResolutionFlow();
|
|
132
125
|
|
|
133
126
|
// Simulate the full user journey: user clicks "Link Document" on a reference entry
|
|
134
|
-
|
|
127
|
+
act(() => { getEventBus().get('reference:link').next({ annotationUri: 'ann-full-chain', searchTerm: 'biodiversity' }); });
|
|
135
128
|
|
|
136
129
|
await waitFor(() => {
|
|
137
130
|
expect(getState().searchModalOpen).toBe(true);
|
|
@@ -142,9 +135,9 @@ describe('Resolution Flow - Search Modal & Body Update Integration', () => {
|
|
|
142
135
|
// ─── onCloseSearchModal ──────────────────────────────────────────────────────
|
|
143
136
|
|
|
144
137
|
it('onCloseSearchModal closes the search modal', async () => {
|
|
145
|
-
const { getState,
|
|
138
|
+
const { getState, getEventBus } = renderResolutionFlow();
|
|
146
139
|
|
|
147
|
-
|
|
140
|
+
act(() => { getEventBus().get('resolution:search-requested').next({ referenceId: 'ref-close', searchTerm: 'test' }); });
|
|
148
141
|
|
|
149
142
|
await waitFor(() => expect(getState().searchModalOpen).toBe(true));
|
|
150
143
|
|
|
@@ -156,9 +149,9 @@ describe('Resolution Flow - Search Modal & Body Update Integration', () => {
|
|
|
156
149
|
});
|
|
157
150
|
|
|
158
151
|
it('onCloseSearchModal does not clear pendingReferenceId (preserves for re-open)', async () => {
|
|
159
|
-
const { getState,
|
|
152
|
+
const { getState, getEventBus } = renderResolutionFlow();
|
|
160
153
|
|
|
161
|
-
|
|
154
|
+
act(() => { getEventBus().get('resolution:search-requested').next({ referenceId: 'ref-persist', searchTerm: 'test' }); });
|
|
162
155
|
await waitFor(() => expect(getState().searchModalOpen).toBe(true));
|
|
163
156
|
|
|
164
157
|
act(() => { getState().onCloseSearchModal(); });
|
|
@@ -171,13 +164,13 @@ describe('Resolution Flow - Search Modal & Body Update Integration', () => {
|
|
|
171
164
|
// ─── annotation:update-body ──────────────────────────────────────────────────
|
|
172
165
|
|
|
173
166
|
it('annotation:update-body calls updateAnnotationBody API', async () => {
|
|
174
|
-
const {
|
|
167
|
+
const { getEventBus } = renderResolutionFlow();
|
|
175
168
|
|
|
176
|
-
|
|
169
|
+
act(() => { getEventBus().get('annotation:update-body').next({
|
|
177
170
|
annotationUri: 'http://localhost:4000/resources/test-resource/annotations/ann-body-1',
|
|
178
171
|
resourceId: 'linked-resource-id',
|
|
179
172
|
operations: [{ op: 'add', item: { id: 'linked-resource-id' } }],
|
|
180
|
-
});
|
|
173
|
+
}); });
|
|
181
174
|
|
|
182
175
|
await waitFor(() => {
|
|
183
176
|
expect(updateAnnotationBodySpy).toHaveBeenCalledTimes(1);
|
|
@@ -185,13 +178,13 @@ describe('Resolution Flow - Search Modal & Body Update Integration', () => {
|
|
|
185
178
|
});
|
|
186
179
|
|
|
187
180
|
it('annotation:update-body passes auth token to API call', async () => {
|
|
188
|
-
const {
|
|
181
|
+
const { getEventBus } = renderResolutionFlow();
|
|
189
182
|
|
|
190
|
-
|
|
183
|
+
act(() => { getEventBus().get('annotation:update-body').next({
|
|
191
184
|
annotationUri: 'http://localhost:4000/resources/test-resource/annotations/ann-auth',
|
|
192
185
|
resourceId: 'resource-id',
|
|
193
186
|
operations: [{ op: 'replace', newItem: { id: 'resource-id' } }],
|
|
194
|
-
});
|
|
187
|
+
}); });
|
|
195
188
|
|
|
196
189
|
await waitFor(() => {
|
|
197
190
|
expect(updateAnnotationBodySpy).toHaveBeenCalled();
|
|
@@ -203,22 +196,22 @@ describe('Resolution Flow - Search Modal & Body Update Integration', () => {
|
|
|
203
196
|
});
|
|
204
197
|
|
|
205
198
|
it('annotation:update-body emits annotation:body-updated on success', async () => {
|
|
206
|
-
const {
|
|
199
|
+
const { getEventBus } = renderResolutionFlow();
|
|
207
200
|
const bodyUpdatedSpy = vi.fn();
|
|
208
201
|
|
|
209
|
-
|
|
202
|
+
const subscription = getEventBus().get('annotation:body-updated').subscribe(bodyUpdatedSpy);
|
|
210
203
|
|
|
211
|
-
|
|
204
|
+
act(() => { getEventBus().get('annotation:update-body').next({
|
|
212
205
|
annotationUri: 'http://localhost:4000/resources/test-resource/annotations/ann-success',
|
|
213
206
|
resourceId: 'resource-id',
|
|
214
207
|
operations: [{ op: 'add', item: { id: 'resource-id' } }],
|
|
215
|
-
});
|
|
208
|
+
}); });
|
|
216
209
|
|
|
217
210
|
await waitFor(() => {
|
|
218
211
|
expect(bodyUpdatedSpy).toHaveBeenCalledTimes(1);
|
|
219
212
|
});
|
|
220
213
|
|
|
221
|
-
|
|
214
|
+
subscription.unsubscribe();
|
|
222
215
|
|
|
223
216
|
expect(bodyUpdatedSpy).toHaveBeenCalledWith({
|
|
224
217
|
annotationUri: 'http://localhost:4000/resources/test-resource/annotations/ann-success',
|
|
@@ -228,22 +221,22 @@ describe('Resolution Flow - Search Modal & Body Update Integration', () => {
|
|
|
228
221
|
it('annotation:update-body emits annotation:body-update-failed on API error', async () => {
|
|
229
222
|
updateAnnotationBodySpy.mockRejectedValue(new Error('Update failed'));
|
|
230
223
|
|
|
231
|
-
const {
|
|
224
|
+
const { getEventBus } = renderResolutionFlow();
|
|
232
225
|
const bodyUpdateFailedSpy = vi.fn();
|
|
233
226
|
|
|
234
|
-
|
|
227
|
+
const subscription = getEventBus().get('annotation:body-update-failed').subscribe(bodyUpdateFailedSpy);
|
|
235
228
|
|
|
236
|
-
|
|
229
|
+
act(() => { getEventBus().get('annotation:update-body').next({
|
|
237
230
|
annotationUri: 'http://localhost:4000/resources/test-resource/annotations/ann-fail',
|
|
238
231
|
resourceId: 'resource-id',
|
|
239
232
|
operations: [{ op: 'remove', item: { id: 'old-id' } }],
|
|
240
|
-
});
|
|
233
|
+
}); });
|
|
241
234
|
|
|
242
235
|
await waitFor(() => {
|
|
243
236
|
expect(bodyUpdateFailedSpy).toHaveBeenCalledTimes(1);
|
|
244
237
|
});
|
|
245
238
|
|
|
246
|
-
|
|
239
|
+
subscription.unsubscribe();
|
|
247
240
|
|
|
248
241
|
expect(bodyUpdateFailedSpy).toHaveBeenCalledWith({
|
|
249
242
|
error: expect.any(Error),
|
|
@@ -251,13 +244,13 @@ describe('Resolution Flow - Search Modal & Body Update Integration', () => {
|
|
|
251
244
|
});
|
|
252
245
|
|
|
253
246
|
it('annotation:update-body called ONCE — no duplicate subscriptions', async () => {
|
|
254
|
-
const {
|
|
247
|
+
const { getEventBus } = renderResolutionFlow();
|
|
255
248
|
|
|
256
|
-
|
|
249
|
+
act(() => { getEventBus().get('annotation:update-body').next({
|
|
257
250
|
annotationUri: 'http://localhost:4000/resources/test-resource/annotations/ann-dedup',
|
|
258
251
|
resourceId: 'resource-id',
|
|
259
252
|
operations: [{ op: 'add', item: { id: 'resource-id' } }],
|
|
260
|
-
});
|
|
253
|
+
}); });
|
|
261
254
|
|
|
262
255
|
await waitFor(() => {
|
|
263
256
|
expect(updateAnnotationBodySpy).toHaveBeenCalledTimes(1);
|
|
@@ -25,14 +25,14 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
|
25
25
|
import { render, waitFor } from '@testing-library/react';
|
|
26
26
|
import { act } from 'react';
|
|
27
27
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
28
|
-
import { SemiontApiClient
|
|
28
|
+
import { SemiontApiClient } from '@semiont/api-client';
|
|
29
|
+
import { resourceUri, accessToken } from '@semiont/core';
|
|
29
30
|
import { EventBusProvider, useEventBus, resetEventBusForTesting } from '../../../contexts/EventBusContext';
|
|
30
31
|
import { useEventSubscriptions } from '../../../contexts/useEventSubscription';
|
|
31
32
|
import { ApiClientProvider } from '../../../contexts/ApiClientContext';
|
|
32
33
|
import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
|
|
33
34
|
import { useResources } from '../../../lib/api-hooks';
|
|
34
|
-
import type { EventMap } from '
|
|
35
|
-
import type { Emitter } from 'mitt';
|
|
35
|
+
import type { EventMap, EventBus } from '@semiont/core';
|
|
36
36
|
|
|
37
37
|
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
38
38
|
|
|
@@ -50,10 +50,10 @@ const CLONE_TOKEN = 'generated-clone-token-xyz';
|
|
|
50
50
|
* The critical invariant under test: useMutation() is called at hook level
|
|
51
51
|
* (inside useResources), not inside the useCallback bodies.
|
|
52
52
|
*/
|
|
53
|
-
function ResourceMutationHarness({ onEventBus }: { onEventBus: (
|
|
53
|
+
function ResourceMutationHarness({ onEventBus }: { onEventBus: (eventBus: EventBus) => void }) {
|
|
54
54
|
const eventBus = useEventBus();
|
|
55
55
|
|
|
56
|
-
// Capture the
|
|
56
|
+
// Capture the eventBus for the test to emit events
|
|
57
57
|
React.useEffect(() => {
|
|
58
58
|
onEventBus(eventBus);
|
|
59
59
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
@@ -91,7 +91,7 @@ function ResourceMutationHarness({ onEventBus }: { onEventBus: (bus: Emitter<Eve
|
|
|
91
91
|
// ─── Test setup ───────────────────────────────────────────────────────────────
|
|
92
92
|
|
|
93
93
|
function renderHarness() {
|
|
94
|
-
let
|
|
94
|
+
let capturedEventBus: EventBus | null = null;
|
|
95
95
|
|
|
96
96
|
const queryClient = new QueryClient({
|
|
97
97
|
defaultOptions: {
|
|
@@ -105,7 +105,7 @@ function renderHarness() {
|
|
|
105
105
|
<ApiClientProvider baseUrl={BASE_URL}>
|
|
106
106
|
<QueryClientProvider client={queryClient}>
|
|
107
107
|
<EventBusProvider>
|
|
108
|
-
<ResourceMutationHarness onEventBus={(
|
|
108
|
+
<ResourceMutationHarness onEventBus={(eventBus) => { capturedEventBus = eventBus; }} />
|
|
109
109
|
</EventBusProvider>
|
|
110
110
|
</QueryClientProvider>
|
|
111
111
|
</ApiClientProvider>
|
|
@@ -113,7 +113,7 @@ function renderHarness() {
|
|
|
113
113
|
);
|
|
114
114
|
|
|
115
115
|
const emit = <K extends keyof EventMap>(event: K, payload: EventMap[K]) => {
|
|
116
|
-
act(() => {
|
|
116
|
+
act(() => { capturedEventBus!.get(event).next(payload); });
|
|
117
117
|
};
|
|
118
118
|
|
|
119
119
|
return { emit };
|
|
@@ -32,7 +32,7 @@ import { EventBusProvider, resetEventBusForTesting } from '../../../contexts/Eve
|
|
|
32
32
|
import { ApiClientProvider } from '../../../contexts/ApiClientContext';
|
|
33
33
|
import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
|
|
34
34
|
import { SSEClient } from '@semiont/api-client';
|
|
35
|
-
import type { components } from '@semiont/
|
|
35
|
+
import type { components } from '@semiont/core';
|
|
36
36
|
|
|
37
37
|
type Annotation = components['schemas']['Annotation'];
|
|
38
38
|
|
|
@@ -60,33 +60,26 @@ vi.mock('../../../contexts/TranslationContext', () => ({
|
|
|
60
60
|
|
|
61
61
|
// Create a mock SSE stream that we can control
|
|
62
62
|
class MockSSEStream {
|
|
63
|
-
private
|
|
64
|
-
private completeHandlers: Array<(finalChunk?: any) => void> = [];
|
|
65
|
-
private errorHandlers: Array<(error: Error) => void> = [];
|
|
63
|
+
constructor(private eventBus: any) {}
|
|
66
64
|
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
close() {
|
|
66
|
+
// Mock close method
|
|
69
67
|
}
|
|
70
68
|
|
|
71
|
-
|
|
72
|
-
this.completeHandlers.push(handler);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
onError(handler: (error: Error) => void) {
|
|
76
|
-
this.errorHandlers.push(handler);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Test helper methods
|
|
69
|
+
// Test helper methods that emit to EventBus
|
|
80
70
|
emitProgress(chunk: any) {
|
|
81
|
-
this.
|
|
71
|
+
this.eventBus.get('detection:progress').next(chunk);
|
|
82
72
|
}
|
|
83
73
|
|
|
84
74
|
emitComplete(finalChunk?: any) {
|
|
85
|
-
|
|
75
|
+
if (finalChunk) {
|
|
76
|
+
this.eventBus.get('detection:progress').next(finalChunk);
|
|
77
|
+
}
|
|
78
|
+
this.eventBus.get('detection:complete').next({});
|
|
86
79
|
}
|
|
87
80
|
|
|
88
81
|
emitError(error: Error) {
|
|
89
|
-
this.
|
|
82
|
+
this.eventBus.get('detection:failed').next({ error });
|
|
90
83
|
}
|
|
91
84
|
}
|
|
92
85
|
|
|
@@ -135,11 +128,11 @@ describe('Detection Progress Flow Integration (Layer 3)', () => {
|
|
|
135
128
|
|
|
136
129
|
beforeEach(() => {
|
|
137
130
|
// Reset event bus for test isolation
|
|
138
|
-
resetEventBusForTesting();
|
|
131
|
+
const eventBus = resetEventBusForTesting();
|
|
139
132
|
vi.clearAllMocks();
|
|
140
133
|
|
|
141
|
-
// Reset mocks
|
|
142
|
-
mockStream = new MockSSEStream();
|
|
134
|
+
// Reset mocks - create stream with eventBus
|
|
135
|
+
mockStream = new MockSSEStream(eventBus);
|
|
143
136
|
|
|
144
137
|
// Spy on SSEClient prototype methods to inject mock stream
|
|
145
138
|
vi.spyOn(SSEClient.prototype, 'detectHighlights').mockReturnValue(mockStream as any);
|
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
|
|
8
8
|
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
|
9
9
|
import { useQueryClient } from '@tanstack/react-query';
|
|
10
|
-
import type { components, ResourceUri } from '@semiont/
|
|
11
|
-
import {
|
|
10
|
+
import type { components, ResourceUri } from '@semiont/core';
|
|
11
|
+
import { resourceAnnotationUri } from '@semiont/core';
|
|
12
|
+
import { getLanguage, getPrimaryRepresentation, getPrimaryMediaType } from '@semiont/api-client';
|
|
12
13
|
import { uriToAnnotationId } from '@semiont/core';
|
|
13
14
|
import { ANNOTATORS } from '@semiont/react-ui';
|
|
14
15
|
import { ErrorBoundary } from '@semiont/react-ui';
|
|
@@ -308,7 +309,7 @@ export function ResourceViewerPage({
|
|
|
308
309
|
try {
|
|
309
310
|
const result = await generateCloneTokenMutation.mutateAsync(rUri);
|
|
310
311
|
const token = result.token;
|
|
311
|
-
eventBus.
|
|
312
|
+
eventBus.get('navigation:router-push').next({ path: `/know/compose?mode=clone&token=${token}`, reason: 'clone' });
|
|
312
313
|
} catch (err) {
|
|
313
314
|
console.error('Failed to generate clone token:', err);
|
|
314
315
|
showError('Failed to generate clone link');
|
|
@@ -345,14 +346,14 @@ export function ResourceViewerPage({
|
|
|
345
346
|
const handleReferenceNavigate = useCallback(({ documentId }: { documentId: string }) => {
|
|
346
347
|
if (routes.resource) {
|
|
347
348
|
const path = routes.resource.replace('[resourceId]', encodeURIComponent(documentId));
|
|
348
|
-
eventBus.
|
|
349
|
+
eventBus.get('navigation:router-push').next({ path, reason: 'reference-link' });
|
|
349
350
|
}
|
|
350
351
|
}, [routes.resource]); // eventBus is stable singleton - never in deps
|
|
351
352
|
|
|
352
353
|
const handleEntityTypeClicked = useCallback(({ entityType }: { entityType: string }) => {
|
|
353
354
|
if (routes.know) {
|
|
354
355
|
const path = `${routes.know}?entityType=${encodeURIComponent(entityType)}`;
|
|
355
|
-
eventBus.
|
|
356
|
+
eventBus.get('navigation:router-push').next({ path, reason: 'entity-type-filter' });
|
|
356
357
|
}
|
|
357
358
|
}, [routes.know]); // eventBus is stable singleton - never in deps
|
|
358
359
|
|
|
@@ -436,7 +437,7 @@ export function ResourceViewerPage({
|
|
|
436
437
|
// Handlers for AnnotationHistory (legacy event-based interaction)
|
|
437
438
|
const handleEventHover = useCallback((annotationId: string | null) => {
|
|
438
439
|
if (annotationId) {
|
|
439
|
-
eventBus.
|
|
440
|
+
eventBus.get('annotation:sparkle').next({ annotationId });
|
|
440
441
|
}
|
|
441
442
|
}, []); // eventBus is stable singleton - never in deps
|
|
442
443
|
|
|
@@ -603,7 +604,7 @@ export function ResourceViewerPage({
|
|
|
603
604
|
const resourceIdSegment = rUri.split('/').pop() || '';
|
|
604
605
|
const nestedUri = `${window.location.origin}/resources/${resourceIdSegment}/annotations/${annotationIdShort}`;
|
|
605
606
|
|
|
606
|
-
eventBus.
|
|
607
|
+
eventBus.get('annotation:update-body').next({
|
|
607
608
|
annotationUri: resourceAnnotationUri(nestedUri),
|
|
608
609
|
resourceId: resourceIdSegment,
|
|
609
610
|
operations: [{
|