@semiont/react-ui 0.2.33-build.80 → 0.2.33-build.82

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 (28) hide show
  1. package/dist/{EventBusContext-7GvDyO0d.d.mts → EventBusContext-CJjL_cCf.d.mts} +67 -19
  2. package/dist/{chunk-ZR4ZV2LY.mjs → chunk-QB52Q7EQ.mjs} +7 -7
  3. package/dist/chunk-QB52Q7EQ.mjs.map +1 -0
  4. package/dist/index.d.mts +194 -208
  5. package/dist/index.mjs +1542 -1637
  6. package/dist/index.mjs.map +1 -1
  7. package/dist/test-utils.d.mts +2 -2
  8. package/dist/test-utils.mjs +1 -1
  9. package/package.json +1 -1
  10. package/src/components/LiveRegion.tsx +18 -18
  11. package/src/components/SessionExpiryBanner.tsx +2 -3
  12. package/src/components/SessionTimer.tsx +2 -2
  13. package/src/components/__tests__/SessionTimer.test.tsx +27 -27
  14. package/src/components/resource/panels/AssessmentPanel.tsx +2 -2
  15. package/src/components/resource/panels/DetectSection.tsx +13 -7
  16. package/src/components/resource/panels/ReferenceEntry.tsx +1 -1
  17. package/src/components/resource/panels/TaggingPanel.tsx +2 -3
  18. package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +1 -1
  19. package/src/features/resource-viewer/__tests__/AnnotationDeletionIntegration.test.tsx +16 -13
  20. package/src/features/resource-viewer/__tests__/DetectionFlowBug.test.tsx +1 -1
  21. package/src/features/resource-viewer/__tests__/DetectionFlowIntegration.test.tsx +13 -13
  22. package/src/features/resource-viewer/__tests__/DetectionProgressDismissal.test.tsx +5 -5
  23. package/src/features/resource-viewer/__tests__/GenerationFlowIntegration.test.tsx +7 -12
  24. package/src/features/resource-viewer/__tests__/ResolutionFlowIntegration.test.tsx +266 -0
  25. package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +11 -12
  26. package/src/features/resource-viewer/__tests__/detection-progress-flow.test.tsx +3 -3
  27. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +132 -93
  28. package/dist/chunk-ZR4ZV2LY.mjs.map +0 -1
@@ -42,7 +42,7 @@ describe('Detection Progress Dismissal Bug', () => {
42
42
  close: vi.fn(),
43
43
  };
44
44
 
45
- vi.spyOn(SSEClient.prototype, 'detectAnnotations').mockReturnValue(mockStream);
45
+ vi.spyOn(SSEClient.prototype, 'detectReferences').mockReturnValue(mockStream);
46
46
  vi.spyOn(SSEClient.prototype, 'detectHighlights').mockReturnValue(mockStream);
47
47
  vi.spyOn(SSEClient.prototype, 'detectComments').mockReturnValue(mockStream);
48
48
  vi.spyOn(SSEClient.prototype, 'detectAssessments').mockReturnValue(mockStream);
@@ -231,11 +231,11 @@ describe('Detection Progress Dismissal Bug', () => {
231
231
  });
232
232
  });
233
233
 
234
- it('FIXED: useEventOperations now forwards final completion chunk data', async () => {
234
+ it('FIXED: useResolutionFlow now forwards final completion chunk data', async () => {
235
235
  /**
236
- * This test verifies the fix for the useEventOperations bug.
236
+ * This test verifies the fix for the useResolutionFlow bug.
237
237
  *
238
- * FIX: useEventOperations.ts stream.onComplete(finalChunk) now emits detection:progress
238
+ * FIX: useResolutionFlow.ts stream.onComplete(finalChunk) now emits detection:progress
239
239
  * with the final chunk data BEFORE emitting detection:complete.
240
240
  *
241
241
  * This ensures the UI can display the final completion message with status:'complete'.
@@ -300,7 +300,7 @@ describe('Detection Progress Dismissal Bug', () => {
300
300
  });
301
301
 
302
302
  // Simulate backend sending final chunk to stream.onComplete(finalChunk)
303
- // useEventOperations should forward this as detection:progress
303
+ // useResolutionFlow should forward this as detection:progress
304
304
  act(() => {
305
305
  onCompleteCallback?.({
306
306
  status: 'complete',
@@ -4,9 +4,8 @@
4
4
  * Tests the COMPLETE generation flow with real component composition:
5
5
  * - EventBusProvider (REAL)
6
6
  * - ApiClientProvider (REAL, with MOCKED client)
7
- * - useGenerationFlow (REAL)
8
- * - useGenerationProgress (REAL)
9
- * - useEventOperations (REAL)
7
+ * - useGenerationFlow (REAL, with inlined progress state)
8
+ * - useResolutionFlow (REAL)
10
9
  * - useEventSubscriptions (REAL)
11
10
  *
12
11
  * This test focuses on ARCHITECTURE and EVENT WIRING:
@@ -24,11 +23,11 @@ import { render, screen, waitFor } from '@testing-library/react';
24
23
  import { act } from 'react';
25
24
  import { useGenerationFlow } from '../../../hooks/useGenerationFlow';
26
25
  import { EventBusProvider, useEventBus, resetEventBusForTesting } from '../../../contexts/EventBusContext';
27
- import { ApiClientProvider, useApiClient } from '../../../contexts/ApiClientContext';
26
+ import { ApiClientProvider } from '../../../contexts/ApiClientContext';
28
27
  import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
29
- import { useEventOperations } from '../../../contexts/useEventOperations';
28
+ import { useResolutionFlow } from '../../../hooks/useResolutionFlow';
30
29
  import { SSEClient } from '@semiont/api-client';
31
- import type { SemiontApiClient, ResourceUri, AnnotationUri } from '@semiont/api-client';
30
+ import type { ResourceUri, AnnotationUri } from '@semiont/api-client';
32
31
  import { resourceUri, annotationUri } from '@semiont/api-client';
33
32
  import type { Emitter } from 'mitt';
34
33
  import type { EventMap } from '../../../contexts/EventBusContext';
@@ -416,13 +415,9 @@ function renderGenerationFlow(
416
415
  // Component to capture EventBus instance and set up event operations
417
416
  function EventBusCapture() {
418
417
  eventBusInstance = useEventBus();
419
- const client = useApiClient();
420
418
 
421
- // Set up event operations (this is what makes the SSE calls)
422
- useEventOperations(eventBusInstance, {
423
- client: client as SemiontApiClient,
424
- resourceUri: testResourceUri,
425
- });
419
+ // Set up resolution flow (annotation:update-body, reference:link)
420
+ useResolutionFlow(testResourceUri);
426
421
 
427
422
  return null;
428
423
  }
@@ -0,0 +1,266 @@
1
+ /**
2
+ * Layer 3: Feature Integration Test - Resolution Flow (search modal & body update)
3
+ *
4
+ * Tests the UNCOVERED half of useResolutionFlow:
5
+ * - reference:link → emits resolution:search-requested
6
+ * - resolution:search-requested → opens search modal with pendingReferenceId
7
+ * - onCloseSearchModal → closes modal
8
+ * - annotation:update-body → calls updateAnnotationBody API
9
+ * - annotation:update-body → emits annotation:body-updated on success
10
+ * - annotation:update-body → emits annotation:body-update-failed on error
11
+ * - auth token passed to updateAnnotationBody
12
+ *
13
+ * The deletion half of useResolutionFlow is covered by AnnotationDeletionIntegration.test.tsx.
14
+ *
15
+ * Uses real providers (EventBus, ApiClient, AuthToken) with mocked API boundary.
16
+ */
17
+
18
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
19
+ import { render, waitFor } from '@testing-library/react';
20
+ import { act } from 'react';
21
+ import { useResolutionFlow } from '../../../hooks/useResolutionFlow';
22
+ import { EventBusProvider, useEventBus, resetEventBusForTesting } from '../../../contexts/EventBusContext';
23
+ import { ApiClientProvider } from '../../../contexts/ApiClientContext';
24
+ import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
25
+ import { SemiontApiClient, resourceUri, accessToken } from '@semiont/api-client';
26
+
27
+ describe('Resolution Flow - Search Modal & Body Update Integration', () => {
28
+ let updateAnnotationBodySpy: ReturnType<typeof vi.fn>;
29
+ const testUri = resourceUri('http://localhost:4000/resources/test-resource');
30
+ const testToken = 'test-resolution-token';
31
+ const testBaseUrl = 'http://localhost:4000';
32
+
33
+ beforeEach(() => {
34
+ vi.clearAllMocks();
35
+ resetEventBusForTesting();
36
+
37
+ updateAnnotationBodySpy = vi.fn().mockResolvedValue({ success: true });
38
+ vi.spyOn(SemiontApiClient.prototype, 'updateAnnotationBody').mockImplementation(updateAnnotationBodySpy);
39
+ });
40
+
41
+ afterEach(() => {
42
+ vi.restoreAllMocks();
43
+ });
44
+
45
+ // ─── Render helper ──────────────────────────────────────────────────────────
46
+
47
+ function renderResolutionFlow() {
48
+ let eventBusInstance: ReturnType<typeof useEventBus> | null = null;
49
+ let lastState: ReturnType<typeof useResolutionFlow> | null = null;
50
+
51
+ function TestComponent() {
52
+ eventBusInstance = useEventBus();
53
+ lastState = useResolutionFlow(testUri);
54
+ return null;
55
+ }
56
+
57
+ render(
58
+ <AuthTokenProvider token={testToken}>
59
+ <EventBusProvider>
60
+ <ApiClientProvider baseUrl={testBaseUrl}>
61
+ <TestComponent />
62
+ </ApiClientProvider>
63
+ </EventBusProvider>
64
+ </AuthTokenProvider>
65
+ );
66
+
67
+ return {
68
+ getState: () => lastState!,
69
+ emit: (event: Parameters<typeof eventBusInstance.emit>[0], payload: Parameters<typeof eventBusInstance.emit>[1]) => {
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
+ },
78
+ };
79
+ }
80
+
81
+ // ─── Initial state ──────────────────────────────────────────────────────────
82
+
83
+ it('starts with search modal closed and no pending reference', () => {
84
+ const { getState } = renderResolutionFlow();
85
+ expect(getState().searchModalOpen).toBe(false);
86
+ expect(getState().pendingReferenceId).toBeNull();
87
+ });
88
+
89
+ // ─── reference:link ─────────────────────────────────────────────────────────
90
+
91
+ it('reference:link emits resolution:search-requested with referenceId and searchTerm', () => {
92
+ const { emit, on, off } = renderResolutionFlow();
93
+ const searchRequestedSpy = vi.fn();
94
+
95
+ on('resolution:search-requested', searchRequestedSpy);
96
+ emit('reference:link', { annotationUri: 'ann-uri-123', searchTerm: 'climate change' });
97
+ off('resolution:search-requested', searchRequestedSpy);
98
+
99
+ expect(searchRequestedSpy).toHaveBeenCalledTimes(1);
100
+ expect(searchRequestedSpy).toHaveBeenCalledWith({
101
+ referenceId: 'ann-uri-123',
102
+ searchTerm: 'climate change',
103
+ });
104
+ });
105
+
106
+ // ─── resolution:search-requested ────────────────────────────────────────────
107
+
108
+ it('resolution:search-requested opens the search modal', async () => {
109
+ const { getState, emit } = renderResolutionFlow();
110
+
111
+ expect(getState().searchModalOpen).toBe(false);
112
+
113
+ emit('resolution:search-requested', { referenceId: 'ref-abc', searchTerm: 'oceans' });
114
+
115
+ await waitFor(() => {
116
+ expect(getState().searchModalOpen).toBe(true);
117
+ });
118
+ });
119
+
120
+ it('resolution:search-requested sets pendingReferenceId', async () => {
121
+ const { getState, emit } = renderResolutionFlow();
122
+
123
+ emit('resolution:search-requested', { referenceId: 'ref-xyz', searchTerm: 'forests' });
124
+
125
+ await waitFor(() => {
126
+ expect(getState().pendingReferenceId).toBe('ref-xyz');
127
+ });
128
+ });
129
+
130
+ it('reference:link → resolution:search-requested chain opens modal end-to-end', async () => {
131
+ const { getState, emit } = renderResolutionFlow();
132
+
133
+ // Simulate the full user journey: user clicks "Link Document" on a reference entry
134
+ emit('reference:link', { annotationUri: 'ann-full-chain', searchTerm: 'biodiversity' });
135
+
136
+ await waitFor(() => {
137
+ expect(getState().searchModalOpen).toBe(true);
138
+ expect(getState().pendingReferenceId).toBe('ann-full-chain');
139
+ });
140
+ });
141
+
142
+ // ─── onCloseSearchModal ──────────────────────────────────────────────────────
143
+
144
+ it('onCloseSearchModal closes the search modal', async () => {
145
+ const { getState, emit } = renderResolutionFlow();
146
+
147
+ emit('resolution:search-requested', { referenceId: 'ref-close', searchTerm: 'test' });
148
+
149
+ await waitFor(() => expect(getState().searchModalOpen).toBe(true));
150
+
151
+ act(() => { getState().onCloseSearchModal(); });
152
+
153
+ await waitFor(() => {
154
+ expect(getState().searchModalOpen).toBe(false);
155
+ });
156
+ });
157
+
158
+ it('onCloseSearchModal does not clear pendingReferenceId (preserves for re-open)', async () => {
159
+ const { getState, emit } = renderResolutionFlow();
160
+
161
+ emit('resolution:search-requested', { referenceId: 'ref-persist', searchTerm: 'test' });
162
+ await waitFor(() => expect(getState().searchModalOpen).toBe(true));
163
+
164
+ act(() => { getState().onCloseSearchModal(); });
165
+ await waitFor(() => expect(getState().searchModalOpen).toBe(false));
166
+
167
+ // pendingReferenceId remains — modal may reopen
168
+ expect(getState().pendingReferenceId).toBe('ref-persist');
169
+ });
170
+
171
+ // ─── annotation:update-body ──────────────────────────────────────────────────
172
+
173
+ it('annotation:update-body calls updateAnnotationBody API', async () => {
174
+ const { emit } = renderResolutionFlow();
175
+
176
+ emit('annotation:update-body', {
177
+ annotationUri: 'http://localhost:4000/resources/test-resource/annotations/ann-body-1',
178
+ resourceId: 'linked-resource-id',
179
+ operations: [{ op: 'add', item: { id: 'linked-resource-id' } }],
180
+ });
181
+
182
+ await waitFor(() => {
183
+ expect(updateAnnotationBodySpy).toHaveBeenCalledTimes(1);
184
+ });
185
+ });
186
+
187
+ it('annotation:update-body passes auth token to API call', async () => {
188
+ const { emit } = renderResolutionFlow();
189
+
190
+ emit('annotation:update-body', {
191
+ annotationUri: 'http://localhost:4000/resources/test-resource/annotations/ann-auth',
192
+ resourceId: 'resource-id',
193
+ operations: [{ op: 'replace', newItem: { id: 'resource-id' } }],
194
+ });
195
+
196
+ await waitFor(() => {
197
+ expect(updateAnnotationBodySpy).toHaveBeenCalled();
198
+ });
199
+
200
+ const callArgs = updateAnnotationBodySpy.mock.calls[0];
201
+ expect(callArgs[2]).toHaveProperty('auth');
202
+ expect(callArgs[2].auth).toBe(accessToken(testToken));
203
+ });
204
+
205
+ it('annotation:update-body emits annotation:body-updated on success', async () => {
206
+ const { emit, on, off } = renderResolutionFlow();
207
+ const bodyUpdatedSpy = vi.fn();
208
+
209
+ on('annotation:body-updated', bodyUpdatedSpy);
210
+
211
+ emit('annotation:update-body', {
212
+ annotationUri: 'http://localhost:4000/resources/test-resource/annotations/ann-success',
213
+ resourceId: 'resource-id',
214
+ operations: [{ op: 'add', item: { id: 'resource-id' } }],
215
+ });
216
+
217
+ await waitFor(() => {
218
+ expect(bodyUpdatedSpy).toHaveBeenCalledTimes(1);
219
+ });
220
+
221
+ off('annotation:body-updated', bodyUpdatedSpy);
222
+
223
+ expect(bodyUpdatedSpy).toHaveBeenCalledWith({
224
+ annotationUri: 'http://localhost:4000/resources/test-resource/annotations/ann-success',
225
+ });
226
+ });
227
+
228
+ it('annotation:update-body emits annotation:body-update-failed on API error', async () => {
229
+ updateAnnotationBodySpy.mockRejectedValue(new Error('Update failed'));
230
+
231
+ const { emit, on, off } = renderResolutionFlow();
232
+ const bodyUpdateFailedSpy = vi.fn();
233
+
234
+ on('annotation:body-update-failed', bodyUpdateFailedSpy);
235
+
236
+ emit('annotation:update-body', {
237
+ annotationUri: 'http://localhost:4000/resources/test-resource/annotations/ann-fail',
238
+ resourceId: 'resource-id',
239
+ operations: [{ op: 'remove', item: { id: 'old-id' } }],
240
+ });
241
+
242
+ await waitFor(() => {
243
+ expect(bodyUpdateFailedSpy).toHaveBeenCalledTimes(1);
244
+ });
245
+
246
+ off('annotation:body-update-failed', bodyUpdateFailedSpy);
247
+
248
+ expect(bodyUpdateFailedSpy).toHaveBeenCalledWith({
249
+ error: expect.any(Error),
250
+ });
251
+ });
252
+
253
+ it('annotation:update-body called ONCE — no duplicate subscriptions', async () => {
254
+ const { emit } = renderResolutionFlow();
255
+
256
+ emit('annotation:update-body', {
257
+ annotationUri: 'http://localhost:4000/resources/test-resource/annotations/ann-dedup',
258
+ resourceId: 'resource-id',
259
+ operations: [{ op: 'add', item: { id: 'resource-id' } }],
260
+ });
261
+
262
+ await waitFor(() => {
263
+ expect(updateAnnotationBodySpy).toHaveBeenCalledTimes(1);
264
+ });
265
+ });
266
+ });
@@ -15,6 +15,7 @@ import { EventBusProvider, resetEventBusForTesting } from '../../../contexts/Eve
15
15
  import { ApiClientProvider } from '../../../contexts/ApiClientContext';
16
16
  import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
17
17
  import { ToastProvider } from '../../../components/Toast';
18
+ import { ThemeProvider } from '../../../contexts/ThemeContext';
18
19
 
19
20
  // jsdom doesn't implement window.matchMedia — mock it for useTheme
20
21
  Object.defineProperty(window, 'matchMedia', {
@@ -76,11 +77,7 @@ vi.mock('@semiont/react-ui', async () => {
76
77
  JsonLdPanel: () => <div data-testid="jsonld-panel">JSON-LD</div>,
77
78
  ErrorBoundary: ({ children }: any) => children,
78
79
  createCancelDetectionHandler: () => vi.fn(),
79
- useGenerationProgress: () => ({
80
- progress: null,
81
- clearProgress: vi.fn(),
82
- }),
83
- useDebouncedCallback: (fn: any) => fn,
80
+ useDebouncedCallback: (fn: any) => fn,
84
81
  supportsDetection: () => false,
85
82
  MakeMeaningEventBusProvider: ({ children }: any) => children,
86
83
  useResourceLoadingAnnouncements: () => ({
@@ -165,13 +162,15 @@ const createMockProps = (overrides?: Partial<ResourceViewerPageProps>): Resource
165
162
  // Test wrapper to provide all required providers
166
163
  const renderWithProviders = (ui: React.ReactElement) => {
167
164
  return render(
168
- <ToastProvider>
169
- <AuthTokenProvider token={null}>
170
- <ApiClientProvider baseUrl="http://localhost:4000">
171
- <EventBusProvider>{ui}</EventBusProvider>
172
- </ApiClientProvider>
173
- </AuthTokenProvider>
174
- </ToastProvider>
165
+ <ThemeProvider>
166
+ <ToastProvider>
167
+ <AuthTokenProvider token={null}>
168
+ <ApiClientProvider baseUrl="http://localhost:4000">
169
+ <EventBusProvider>{ui}</EventBusProvider>
170
+ </ApiClientProvider>
171
+ </AuthTokenProvider>
172
+ </ToastProvider>
173
+ </ThemeProvider>
175
174
  );
176
175
  };
177
176
 
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * Layer 3 Integration Test: Detection Progress Flow UI/UX
3
3
  *
4
- * Tests the complete data flow from UI → EventBus → useEventOperations → SSE (mocked)
4
+ * Tests the complete data flow from UI → EventBus → useResolutionFlow → SSE (mocked)
5
5
  *
6
6
  * This test uses COMPOSITION instead of mocking:
7
7
  * - Real React components composed together (useDetectionFlow + HighlightPanel + DetectSection)
8
8
  * - Real EventBus (mitt) passed via context
9
- * - Real useEventOperations hook with mock API client passed as prop
9
+ * - Real useResolutionFlow hook with mock API client passed as prop
10
10
  * - Mock SSE stream (simulated API responses) provided via composition
11
11
  *
12
12
  * This test focuses on USER EXPERIENCE:
@@ -145,7 +145,7 @@ describe('Detection Progress Flow Integration (Layer 3)', () => {
145
145
  vi.spyOn(SSEClient.prototype, 'detectHighlights').mockReturnValue(mockStream as any);
146
146
  vi.spyOn(SSEClient.prototype, 'detectAssessments').mockReturnValue(mockStream as any);
147
147
  vi.spyOn(SSEClient.prototype, 'detectComments').mockReturnValue(mockStream as any);
148
- vi.spyOn(SSEClient.prototype, 'detectAnnotations').mockReturnValue(mockStream as any);
148
+ vi.spyOn(SSEClient.prototype, 'detectReferences').mockReturnValue(mockStream as any);
149
149
 
150
150
  mockAnnotations = [];
151
151
  });