@semiont/react-ui 0.4.13 → 0.4.15
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/README.md +18 -12
- package/dist/KnowledgeBaseSessionContext-CpYaCbnC.d.mts +174 -0
- package/dist/{PdfAnnotationCanvas.client-CW6SKH2U.mjs → PdfAnnotationCanvas.client-CHDCGQBR.mjs} +3 -3
- package/dist/{chunk-HNZOXH4L.mjs → chunk-OZICDVH7.mjs} +5 -3
- package/dist/chunk-OZICDVH7.mjs.map +1 -0
- package/dist/chunk-R2U7P4TK.mjs +865 -0
- package/dist/chunk-R2U7P4TK.mjs.map +1 -0
- package/dist/{chunk-BQJWOK4C.mjs → chunk-VN5NY4SN.mjs} +9 -8
- package/dist/chunk-VN5NY4SN.mjs.map +1 -0
- package/dist/index.d.mts +147 -171
- package/dist/index.mjs +2215 -1961
- package/dist/index.mjs.map +1 -1
- package/dist/test-utils.d.mts +13 -62
- package/dist/test-utils.mjs +40 -21
- package/dist/test-utils.mjs.map +1 -1
- package/package.json +5 -3
- package/src/components/ProtectedErrorBoundary.tsx +95 -0
- package/src/components/Toolbar.tsx +13 -13
- package/src/components/__tests__/ProtectedErrorBoundary.test.tsx +197 -0
- package/src/components/modals/PermissionDeniedModal.tsx +140 -0
- package/src/components/modals/ReferenceWizardModal.tsx +3 -2
- package/src/components/modals/SessionExpiredModal.tsx +101 -0
- package/src/components/modals/__tests__/PermissionDeniedModal.test.tsx +150 -0
- package/src/components/modals/__tests__/SessionExpiredModal.test.tsx +115 -0
- package/src/components/resource/AnnotationHistory.tsx +5 -6
- package/src/components/resource/HistoryEvent.tsx +7 -7
- package/src/components/resource/__tests__/AnnotationHistory.test.tsx +33 -34
- package/src/components/resource/__tests__/HistoryEvent.test.tsx +17 -19
- package/src/components/resource/__tests__/event-formatting.test.ts +70 -94
- package/src/components/resource/event-formatting.ts +56 -56
- package/src/components/resource/panels/CollaborationPanel.tsx +9 -1
- package/src/components/resource/panels/ReferenceEntry.tsx +7 -5
- package/src/components/resource/panels/ResourceInfoPanel.tsx +8 -6
- package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +12 -12
- package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +1 -0
- package/src/features/resource-viewer/__tests__/AnnotationCreationPending.test.tsx +1 -1
- package/src/features/resource-viewer/__tests__/AnnotationDeletionIntegration.test.tsx +4 -4
- package/src/features/resource-viewer/__tests__/AnnotationProgressDismissal.test.tsx +5 -10
- package/src/features/resource-viewer/__tests__/BindFlowIntegration.test.tsx +23 -54
- package/src/features/resource-viewer/__tests__/DetectionFlowBug.test.tsx +6 -6
- package/src/features/resource-viewer/__tests__/DetectionFlowIntegration.test.tsx +7 -19
- package/src/features/resource-viewer/__tests__/ToastNotifications.test.tsx +1 -1
- package/src/features/resource-viewer/__tests__/YieldFlowIntegration.test.tsx +18 -44
- package/src/features/resource-viewer/__tests__/annotation-progress-flow.test.tsx +6 -6
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +31 -26
- package/src/styles/patterns/panels-base.css +12 -0
- package/dist/TranslationManager-CudgH3gw.d.mts +0 -107
- package/dist/chunk-BQJWOK4C.mjs.map +0 -1
- package/dist/chunk-HNZOXH4L.mjs.map +0 -1
- package/dist/chunk-OL5UST25.mjs +0 -413
- package/dist/chunk-OL5UST25.mjs.map +0 -1
- /package/dist/{PdfAnnotationCanvas.client-CW6SKH2U.mjs.map → PdfAnnotationCanvas.client-CHDCGQBR.mjs.map} +0 -0
|
@@ -5,6 +5,7 @@ import '@testing-library/jest-dom';
|
|
|
5
5
|
import { renderWithProviders } from '../../../../test-utils';
|
|
6
6
|
import userEvent from '@testing-library/user-event';
|
|
7
7
|
import type { components } from '@semiont/core';
|
|
8
|
+
import { SemiontApiClient } from '@semiont/api-client';
|
|
8
9
|
import type { RouteBuilder } from '../../../../contexts/RoutingContext';
|
|
9
10
|
|
|
10
11
|
type Annotation = components['schemas']['Annotation'];
|
|
@@ -303,28 +304,27 @@ describe('ReferenceEntry', () => {
|
|
|
303
304
|
expect(unlinkButton).not.toBeInTheDocument();
|
|
304
305
|
});
|
|
305
306
|
|
|
306
|
-
it('should
|
|
307
|
+
it('should call client.bind.body on unlink click', async () => {
|
|
307
308
|
mockIsBodyResolved.mockReturnValue(true);
|
|
308
309
|
mockGetBodySource.mockReturnValue('linked-doc');
|
|
309
|
-
const unlinkHandler = vi.fn();
|
|
310
310
|
|
|
311
|
-
const
|
|
311
|
+
const bindSpy = vi.spyOn(SemiontApiClient.prototype, 'bindAnnotation').mockResolvedValue({ correlationId: 'c1' });
|
|
312
|
+
|
|
313
|
+
const { container } = renderWithProviders(
|
|
312
314
|
<ReferenceEntry {...defaultProps} annotateMode={true} />,
|
|
313
|
-
{ returnEventBus: true }
|
|
314
315
|
);
|
|
315
316
|
|
|
316
|
-
const subscription = eventBus!.get('bind:update-body').subscribe(unlinkHandler);
|
|
317
|
-
|
|
318
317
|
const unlinkButton = container.querySelector('.semiont-reference-unlink')!;
|
|
319
318
|
await userEvent.click(unlinkButton);
|
|
320
319
|
|
|
321
|
-
expect(
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
operations: [{ op: 'remove', item: { type: 'SpecificResource', source: 'linked-doc' } }],
|
|
325
|
-
|
|
320
|
+
expect(bindSpy).toHaveBeenCalledWith(
|
|
321
|
+
'resource-1',
|
|
322
|
+
'ref-1',
|
|
323
|
+
{ operations: [{ op: 'remove', item: { type: 'SpecificResource', source: 'linked-doc' } }] },
|
|
324
|
+
expect.anything(),
|
|
325
|
+
);
|
|
326
326
|
|
|
327
|
-
|
|
327
|
+
bindSpy.mockRestore();
|
|
328
328
|
});
|
|
329
329
|
});
|
|
330
330
|
|
|
@@ -122,6 +122,7 @@ const renderWithEventBus = (component: React.ReactElement, tracker?: ReturnType<
|
|
|
122
122
|
|
|
123
123
|
describe('ResourceInfoPanel Component', () => {
|
|
124
124
|
const defaultProps = {
|
|
125
|
+
resourceId: 'test-resource-id',
|
|
125
126
|
documentEntityTypes: [],
|
|
126
127
|
documentLocale: undefined,
|
|
127
128
|
primaryMediaType: undefined,
|
|
@@ -248,7 +248,7 @@ describe('Annotation creation clears pendingAnnotation', () => {
|
|
|
248
248
|
const createdListener = vi.fn();
|
|
249
249
|
// Set listener after first render so eventBus is captured
|
|
250
250
|
await waitFor(() => expect(getEventBus()).toBeDefined());
|
|
251
|
-
const subscription = getEventBus().get('mark:
|
|
251
|
+
const subscription = getEventBus().get('mark:create-ok').subscribe(createdListener);
|
|
252
252
|
|
|
253
253
|
act(() => {
|
|
254
254
|
emit('mark:requested', { selector: TEXT_SELECTOR, motivation: 'linking' });
|
|
@@ -27,6 +27,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
|
27
27
|
import { render, waitFor } from '@testing-library/react';
|
|
28
28
|
import { act } from 'react';
|
|
29
29
|
import { useMarkFlow } from '../../../hooks/useMarkFlow';
|
|
30
|
+
import { useStoreTokenSync } from '../../../hooks/useStoreTokenSync';
|
|
30
31
|
import { EventBusProvider, useEventBus } from '../../../contexts/EventBusContext';
|
|
31
32
|
|
|
32
33
|
// Mock Toast module to prevent "useToast must be used within a ToastProvider" errors
|
|
@@ -69,8 +70,7 @@ describe('Annotation Deletion - Feature Integration', () => {
|
|
|
69
70
|
|
|
70
71
|
function TestComponent() {
|
|
71
72
|
eventBusInstance = useEventBus();
|
|
72
|
-
//
|
|
73
|
-
// (handles mark:delete mark:create annotate:detect-request, etc.)
|
|
73
|
+
useStoreTokenSync(); // Syncs auth token to namespace getToken
|
|
74
74
|
useMarkFlow(testId);
|
|
75
75
|
return null;
|
|
76
76
|
}
|
|
@@ -138,7 +138,7 @@ describe('Annotation Deletion - Feature Integration', () => {
|
|
|
138
138
|
const deletedListener = vi.fn();
|
|
139
139
|
|
|
140
140
|
// Subscribe to success event
|
|
141
|
-
eventBus.get('mark:
|
|
141
|
+
eventBus.get('mark:delete-ok').subscribe(deletedListener);
|
|
142
142
|
|
|
143
143
|
emitDelete('annotation-789');
|
|
144
144
|
|
|
@@ -175,7 +175,7 @@ describe('Annotation Deletion - Feature Integration', () => {
|
|
|
175
175
|
// Verify failure event was emitted
|
|
176
176
|
await waitFor(() => {
|
|
177
177
|
expect(failedListener).toHaveBeenCalledWith({
|
|
178
|
-
|
|
178
|
+
message: expect.any(String),
|
|
179
179
|
});
|
|
180
180
|
});
|
|
181
181
|
});
|
|
@@ -25,7 +25,7 @@ import { useMarkFlow } from '../../../hooks/useMarkFlow';
|
|
|
25
25
|
import { EventBusProvider, useEventBus } from '../../../contexts/EventBusContext';
|
|
26
26
|
import { ApiClientProvider } from '../../../contexts/ApiClientContext';
|
|
27
27
|
import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
|
|
28
|
-
import {
|
|
28
|
+
import { SemiontApiClient } from '@semiont/api-client';
|
|
29
29
|
import { resourceId } from '@semiont/core';
|
|
30
30
|
|
|
31
31
|
// Mock Toast module to prevent "useToast must be used within a ToastProvider" errors
|
|
@@ -39,20 +39,15 @@ vi.mock('../../../components/Toast', () => ({
|
|
|
39
39
|
}));
|
|
40
40
|
|
|
41
41
|
describe('Detection Progress Dismissal Bug', () => {
|
|
42
|
-
let mockStream: any;
|
|
43
42
|
const rUri = resourceId('test');
|
|
44
43
|
|
|
45
44
|
beforeEach(() => {
|
|
46
45
|
vi.clearAllMocks();
|
|
47
46
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
vi.spyOn(SSEClient.prototype, 'markReferences').mockReturnValue(mockStream);
|
|
53
|
-
vi.spyOn(SSEClient.prototype, 'markHighlights').mockReturnValue(mockStream);
|
|
54
|
-
vi.spyOn(SSEClient.prototype, 'markComments').mockReturnValue(mockStream);
|
|
55
|
-
vi.spyOn(SSEClient.prototype, 'markAssessments').mockReturnValue(mockStream);
|
|
47
|
+
vi.spyOn(SemiontApiClient.prototype, 'annotateReferences').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
|
|
48
|
+
vi.spyOn(SemiontApiClient.prototype, 'annotateHighlights').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
|
|
49
|
+
vi.spyOn(SemiontApiClient.prototype, 'annotateComments').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
|
|
50
|
+
vi.spyOn(SemiontApiClient.prototype, 'annotateAssessments').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
|
|
56
51
|
});
|
|
57
52
|
|
|
58
53
|
afterEach(() => {
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
* Layer 3: Feature Integration Test - Bind Flow (body update)
|
|
3
3
|
*
|
|
4
4
|
* Tests the write side of useBindFlow:
|
|
5
|
-
* - bind:update-body → calls bindAnnotation API
|
|
6
|
-
* - bind:update-body → emits bind:body-updated on success
|
|
5
|
+
* - bind:update-body → calls http.bindAnnotation API (plain POST)
|
|
7
6
|
* - bind:update-body → emits bind:body-update-failed on error
|
|
8
7
|
* - auth token passed to bindAnnotation
|
|
9
8
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* After the UNIFIED-STREAM migration, bind is a plain POST returning
|
|
10
|
+
* {correlationId}. The state change arrives on the events-stream as
|
|
11
|
+
* mark:body-updated. These tests focus on the POST call, not the
|
|
12
|
+
* events-stream delivery (which is tested in AnnotationStore tests).
|
|
13
13
|
*
|
|
14
14
|
* Uses real providers (EventBus, ApiClient, AuthToken) with mocked API boundary.
|
|
15
15
|
*/
|
|
@@ -21,14 +21,15 @@ import { useBindFlow } from '../../../hooks/useBindFlow';
|
|
|
21
21
|
import { EventBusProvider, useEventBus } from '../../../contexts/EventBusContext';
|
|
22
22
|
import { ApiClientProvider } from '../../../contexts/ApiClientContext';
|
|
23
23
|
import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
|
|
24
|
-
import {
|
|
25
|
-
import { resourceId,
|
|
24
|
+
import { SemiontApiClient } from '@semiont/api-client';
|
|
25
|
+
import { resourceId, annotationId } from '@semiont/core';
|
|
26
|
+
|
|
27
|
+
const mockShowError = vi.fn();
|
|
26
28
|
|
|
27
|
-
// Mock Toast module to prevent "useToast must be used within a ToastProvider" errors
|
|
28
29
|
vi.mock('../../../components/Toast', () => ({
|
|
29
30
|
useToast: () => ({
|
|
30
31
|
showSuccess: vi.fn(),
|
|
31
|
-
showError:
|
|
32
|
+
showError: mockShowError,
|
|
32
33
|
showInfo: vi.fn(),
|
|
33
34
|
showWarning: vi.fn(),
|
|
34
35
|
}),
|
|
@@ -43,11 +44,9 @@ describe('Bind Flow - Body Update Integration', () => {
|
|
|
43
44
|
beforeEach(() => {
|
|
44
45
|
vi.clearAllMocks();
|
|
45
46
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
});
|
|
50
|
-
vi.spyOn(SSEClient.prototype, 'bindAnnotation').mockImplementation(bindAnnotationSpy as any);
|
|
47
|
+
// Mock the HTTP bindAnnotation method (plain POST, returns {correlationId})
|
|
48
|
+
bindAnnotationSpy = vi.fn().mockResolvedValue({ correlationId: 'corr-test' });
|
|
49
|
+
vi.spyOn(SemiontApiClient.prototype, 'bindAnnotation').mockImplementation(bindAnnotationSpy as any);
|
|
51
50
|
});
|
|
52
51
|
|
|
53
52
|
afterEach(() => {
|
|
@@ -82,10 +81,11 @@ describe('Bind Flow - Body Update Integration', () => {
|
|
|
82
81
|
|
|
83
82
|
// ─── bind:update-body ──────────────────────────────────────────────────
|
|
84
83
|
|
|
85
|
-
it('bind:update-body calls bindAnnotation
|
|
84
|
+
it('bind:update-body calls http.bindAnnotation (plain POST)', async () => {
|
|
86
85
|
const { getEventBus } = renderBindFlow();
|
|
87
86
|
|
|
88
87
|
act(() => { getEventBus().get('bind:update-body').next({
|
|
88
|
+
correlationId: 'corr-1',
|
|
89
89
|
annotationId: annotationId('ann-body-1'),
|
|
90
90
|
resourceId: resourceId('linked-resource-id'),
|
|
91
91
|
operations: [{ op: 'add', item: { type: 'SpecificResource' as const, source: 'linked-resource-id' } }],
|
|
@@ -100,6 +100,7 @@ describe('Bind Flow - Body Update Integration', () => {
|
|
|
100
100
|
const { getEventBus } = renderBindFlow();
|
|
101
101
|
|
|
102
102
|
act(() => { getEventBus().get('bind:update-body').next({
|
|
103
|
+
correlationId: 'corr-2',
|
|
103
104
|
annotationId: annotationId('ann-auth'),
|
|
104
105
|
resourceId: resourceId('resource-id'),
|
|
105
106
|
operations: [{ op: 'replace', newItem: { type: 'SpecificResource' as const, source: 'resource-id' } }],
|
|
@@ -111,57 +112,24 @@ describe('Bind Flow - Body Update Integration', () => {
|
|
|
111
112
|
|
|
112
113
|
const callArgs = bindAnnotationSpy.mock.calls[0];
|
|
113
114
|
expect(callArgs[3]).toHaveProperty('auth');
|
|
114
|
-
expect(callArgs[3].auth).toBe(accessToken(testToken));
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('bind:update-body emits bind:body-updated on success', async () => {
|
|
118
|
-
const { getEventBus } = renderBindFlow();
|
|
119
|
-
const bodyUpdatedSpy = vi.fn();
|
|
120
|
-
|
|
121
|
-
const subscription = getEventBus().get('bind:body-updated').subscribe(bodyUpdatedSpy);
|
|
122
|
-
|
|
123
|
-
act(() => { getEventBus().get('bind:update-body').next({
|
|
124
|
-
annotationId: annotationId('ann-success'),
|
|
125
|
-
resourceId: resourceId('resource-id'),
|
|
126
|
-
operations: [{ op: 'add', item: { type: 'SpecificResource' as const, source: 'resource-id' } }],
|
|
127
|
-
}); });
|
|
128
|
-
|
|
129
|
-
await waitFor(() => {
|
|
130
|
-
expect(bodyUpdatedSpy).toHaveBeenCalledTimes(1);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
subscription.unsubscribe();
|
|
134
|
-
|
|
135
|
-
expect(bodyUpdatedSpy).toHaveBeenCalledWith({
|
|
136
|
-
annotationId: annotationId('ann-success'),
|
|
137
|
-
});
|
|
138
115
|
});
|
|
139
116
|
|
|
140
|
-
it('bind:update-body
|
|
141
|
-
bindAnnotationSpy.
|
|
142
|
-
queueMicrotask(() => opts.eventBus.get('bind:failed').next({ error: new Error('Update failed') }));
|
|
143
|
-
return { close: vi.fn() };
|
|
144
|
-
});
|
|
117
|
+
it('bind:update-body shows error toast on API error', async () => {
|
|
118
|
+
bindAnnotationSpy.mockRejectedValueOnce(new Error('Update failed'));
|
|
145
119
|
|
|
146
120
|
const { getEventBus } = renderBindFlow();
|
|
147
|
-
const bodyUpdateFailedSpy = vi.fn();
|
|
148
|
-
|
|
149
|
-
const subscription = getEventBus().get('bind:body-update-failed').subscribe(bodyUpdateFailedSpy);
|
|
150
121
|
|
|
151
122
|
act(() => { getEventBus().get('bind:update-body').next({
|
|
123
|
+
correlationId: 'corr-3',
|
|
152
124
|
annotationId: annotationId('ann-fail'),
|
|
153
125
|
resourceId: resourceId('resource-id'),
|
|
154
126
|
operations: [{ op: 'remove', item: { type: 'SpecificResource' as const, source: 'old-id' } }],
|
|
155
127
|
}); });
|
|
156
128
|
|
|
157
129
|
await waitFor(() => {
|
|
158
|
-
expect(
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
subscription.unsubscribe();
|
|
162
|
-
|
|
163
|
-
expect(bodyUpdateFailedSpy).toHaveBeenCalledWith({
|
|
164
|
-
error: expect.any(Error),
|
|
130
|
+
expect(mockShowError).toHaveBeenCalledWith(
|
|
131
|
+
expect.stringContaining('Update failed'),
|
|
132
|
+
);
|
|
165
133
|
});
|
|
166
134
|
});
|
|
167
135
|
|
|
@@ -169,6 +137,7 @@ describe('Bind Flow - Body Update Integration', () => {
|
|
|
169
137
|
const { getEventBus } = renderBindFlow();
|
|
170
138
|
|
|
171
139
|
act(() => { getEventBus().get('bind:update-body').next({
|
|
140
|
+
correlationId: 'corr-4',
|
|
172
141
|
annotationId: annotationId('ann-dedup'),
|
|
173
142
|
resourceId: resourceId('resource-id'),
|
|
174
143
|
operations: [{ op: 'add', item: { type: 'SpecificResource' as const, source: 'resource-id' } }],
|
|
@@ -17,7 +17,7 @@ import { useMarkFlow } from '../../../hooks/useMarkFlow';
|
|
|
17
17
|
import { EventBusProvider, useEventBus } from '../../../contexts/EventBusContext';
|
|
18
18
|
import { ApiClientProvider } from '../../../contexts/ApiClientContext';
|
|
19
19
|
import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
|
|
20
|
-
import {
|
|
20
|
+
import { SemiontApiClient } from '@semiont/api-client';
|
|
21
21
|
|
|
22
22
|
// Mock Toast module to prevent "useToast must be used within a ToastProvider" errors
|
|
23
23
|
vi.mock('../../../components/Toast', () => ({
|
|
@@ -33,11 +33,11 @@ describe('REPRODUCING BUG: Detection state not updating', () => {
|
|
|
33
33
|
beforeEach(() => {
|
|
34
34
|
vi.clearAllMocks();
|
|
35
35
|
|
|
36
|
-
// Minimal mock
|
|
37
|
-
vi.spyOn(
|
|
38
|
-
vi.spyOn(
|
|
39
|
-
vi.spyOn(
|
|
40
|
-
vi.spyOn(
|
|
36
|
+
// Minimal mock — namespace methods call these HTTP methods internally
|
|
37
|
+
vi.spyOn(SemiontApiClient.prototype, 'annotateReferences').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
|
|
38
|
+
vi.spyOn(SemiontApiClient.prototype, 'annotateHighlights').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
|
|
39
|
+
vi.spyOn(SemiontApiClient.prototype, 'annotateComments').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
|
|
40
|
+
vi.spyOn(SemiontApiClient.prototype, 'annotateAssessments').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
|
|
41
41
|
});
|
|
42
42
|
|
|
43
43
|
afterEach(() => {
|
|
@@ -29,7 +29,7 @@ import { useMarkFlow } from '../../../hooks/useMarkFlow';
|
|
|
29
29
|
import { EventBusProvider, useEventBus } from '../../../contexts/EventBusContext';
|
|
30
30
|
import { ApiClientProvider } from '../../../contexts/ApiClientContext';
|
|
31
31
|
import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
|
|
32
|
-
import {
|
|
32
|
+
import { SemiontApiClient } from '@semiont/api-client';
|
|
33
33
|
import type { Motivation } from '@semiont/core';
|
|
34
34
|
import { resourceId } from '@semiont/core';
|
|
35
35
|
import type { Emitter } from 'mitt';
|
|
@@ -45,15 +45,7 @@ vi.mock('../../../components/Toast', () => ({
|
|
|
45
45
|
}));
|
|
46
46
|
import type { EventMap } from '@semiont/core';
|
|
47
47
|
|
|
48
|
-
// Mock SSE stream - SSE now emits directly to EventBus, no callbacks
|
|
49
|
-
const createMockSSEStream = () => {
|
|
50
|
-
return {
|
|
51
|
-
close: vi.fn(),
|
|
52
|
-
};
|
|
53
|
-
};
|
|
54
|
-
|
|
55
48
|
describe('Detection Flow - Feature Integration', () => {
|
|
56
|
-
let mockStream: ReturnType<typeof createMockSSEStream>;
|
|
57
49
|
let markReferencesSpy: any;
|
|
58
50
|
let markHighlightsSpy: any;
|
|
59
51
|
let detectCommentsSpy: any;
|
|
@@ -61,14 +53,11 @@ describe('Detection Flow - Feature Integration', () => {
|
|
|
61
53
|
beforeEach(() => {
|
|
62
54
|
vi.clearAllMocks();
|
|
63
55
|
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
markHighlightsSpy = vi.spyOn(SSEClient.prototype, 'markHighlights').mockReturnValue(mockStream as any);
|
|
70
|
-
detectCommentsSpy = vi.spyOn(SSEClient.prototype, 'markComments').mockReturnValue(mockStream as any);
|
|
71
|
-
vi.spyOn(SSEClient.prototype, 'markAssessments').mockReturnValue(mockStream as any);
|
|
56
|
+
// Spy on SemiontApiClient prototype HTTP methods (namespace methods call these)
|
|
57
|
+
markReferencesSpy = vi.spyOn(SemiontApiClient.prototype, 'annotateReferences').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
|
|
58
|
+
markHighlightsSpy = vi.spyOn(SemiontApiClient.prototype, 'annotateHighlights').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
|
|
59
|
+
detectCommentsSpy = vi.spyOn(SemiontApiClient.prototype, 'annotateComments').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
|
|
60
|
+
vi.spyOn(SemiontApiClient.prototype, 'annotateAssessments').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
|
|
72
61
|
});
|
|
73
62
|
|
|
74
63
|
afterEach(() => {
|
|
@@ -298,8 +287,7 @@ describe('Detection Flow - Feature Integration', () => {
|
|
|
298
287
|
|
|
299
288
|
// Reset for next test
|
|
300
289
|
vi.clearAllMocks();
|
|
301
|
-
|
|
302
|
-
detectCommentsSpy.mockReturnValue(mockStream);
|
|
290
|
+
detectCommentsSpy.mockResolvedValue({ correlationId: 'c2', jobId: 'j2' });
|
|
303
291
|
|
|
304
292
|
// Test commenting
|
|
305
293
|
act(() => {
|
|
@@ -158,7 +158,7 @@ describe('Toast Notifications - Verifies Toast Integration', () => {
|
|
|
158
158
|
// Emit generation failed event
|
|
159
159
|
act(() => {
|
|
160
160
|
eventBusInstance.get('yield:failed').next({
|
|
161
|
-
error:
|
|
161
|
+
error: 'Failed to generate document',
|
|
162
162
|
});
|
|
163
163
|
});
|
|
164
164
|
|
|
@@ -26,10 +26,9 @@ import { EventBusProvider, useEventBus } from '../../../contexts/EventBusContext
|
|
|
26
26
|
import { ApiClientProvider } from '../../../contexts/ApiClientContext';
|
|
27
27
|
import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
|
|
28
28
|
import { useBindFlow } from '../../../hooks/useBindFlow';
|
|
29
|
-
import {
|
|
29
|
+
import { SemiontApiClient } from '@semiont/api-client';
|
|
30
30
|
import type { AnnotationId, ResourceId } from '@semiont/core';
|
|
31
31
|
import { resourceId, annotationId } from '@semiont/core';
|
|
32
|
-
import type { Emitter } from 'mitt';
|
|
33
32
|
import type { EventMap } from '@semiont/core';
|
|
34
33
|
|
|
35
34
|
// Mock Toast module to prevent "useToast must be used within a ToastProvider" errors
|
|
@@ -42,15 +41,7 @@ vi.mock('../../../components/Toast', () => ({
|
|
|
42
41
|
}),
|
|
43
42
|
}));
|
|
44
43
|
|
|
45
|
-
// Mock SSE stream - SSE now emits directly to EventBus, no callbacks
|
|
46
|
-
const createMockGenerationStream = () => {
|
|
47
|
-
return {
|
|
48
|
-
close: vi.fn(),
|
|
49
|
-
};
|
|
50
|
-
};
|
|
51
|
-
|
|
52
44
|
describe('Generation Flow - Feature Integration', () => {
|
|
53
|
-
let mockStream: ReturnType<typeof createMockGenerationStream>;
|
|
54
45
|
let generateResourceSpy: any;
|
|
55
46
|
let mockShowSuccess: ReturnType<typeof vi.fn>;
|
|
56
47
|
let mockShowError: ReturnType<typeof vi.fn>;
|
|
@@ -59,11 +50,8 @@ describe('Generation Flow - Feature Integration', () => {
|
|
|
59
50
|
beforeEach(() => {
|
|
60
51
|
vi.clearAllMocks();
|
|
61
52
|
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
// Spy on SSEClient prototype method
|
|
66
|
-
generateResourceSpy = vi.spyOn(SSEClient.prototype, 'yieldResource').mockReturnValue(mockStream as any);
|
|
53
|
+
// Spy on SemiontApiClient prototype HTTP method (namespace methods call this)
|
|
54
|
+
generateResourceSpy = vi.spyOn(SemiontApiClient.prototype, 'yieldResourceFromAnnotation').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
|
|
67
55
|
|
|
68
56
|
// Mock callbacks
|
|
69
57
|
mockShowSuccess = vi.fn();
|
|
@@ -107,17 +95,13 @@ describe('Generation Flow - Feature Integration', () => {
|
|
|
107
95
|
expect(generateResourceSpy).toHaveBeenCalledWith(
|
|
108
96
|
testResourceId,
|
|
109
97
|
testAnnotationId,
|
|
110
|
-
{
|
|
98
|
+
expect.objectContaining({
|
|
111
99
|
title: 'Generated Document',
|
|
112
100
|
prompt: 'Create a comprehensive document',
|
|
113
101
|
language: 'en',
|
|
114
102
|
temperature: 0.7,
|
|
115
103
|
maxTokens: 2000,
|
|
116
|
-
|
|
117
|
-
sourceText: 'Reference text from the document',
|
|
118
|
-
entityTypes: ['Person', 'Organization'],
|
|
119
|
-
},
|
|
120
|
-
},
|
|
104
|
+
}),
|
|
121
105
|
expect.objectContaining({ auth: undefined })
|
|
122
106
|
);
|
|
123
107
|
});
|
|
@@ -295,7 +279,7 @@ describe('Generation Flow - Feature Integration', () => {
|
|
|
295
279
|
|
|
296
280
|
// Emit failure
|
|
297
281
|
act(() => {
|
|
298
|
-
getEventBus().get('yield:failed').next({ error:
|
|
282
|
+
getEventBus().get('yield:failed').next({ error: 'Network error' });
|
|
299
283
|
});
|
|
300
284
|
|
|
301
285
|
// Verify: progress cleared and not generating
|
|
@@ -305,18 +289,14 @@ describe('Generation Flow - Feature Integration', () => {
|
|
|
305
289
|
});
|
|
306
290
|
});
|
|
307
291
|
|
|
308
|
-
it('should only call API once even with multiple
|
|
292
|
+
it('should only call API once even with multiple renders', async () => {
|
|
309
293
|
const testResourceId = resourceId('test-resource');
|
|
310
294
|
const testAnnotationId = annotationId('test-annotation');
|
|
311
295
|
|
|
312
|
-
const { emitGenerationStart
|
|
296
|
+
const { emitGenerationStart } = renderYieldFlow(
|
|
313
297
|
testResourceId
|
|
314
298
|
);
|
|
315
299
|
|
|
316
|
-
// Add an additional event listener (simulating multiple subscribers)
|
|
317
|
-
const additionalListener = vi.fn();
|
|
318
|
-
const subscription = getEventBus().get('yield:request').subscribe(additionalListener);
|
|
319
|
-
|
|
320
300
|
// Trigger generation
|
|
321
301
|
act(() => {
|
|
322
302
|
emitGenerationStart(testAnnotationId, testResourceId, {
|
|
@@ -330,13 +310,8 @@ describe('Generation Flow - Feature Integration', () => {
|
|
|
330
310
|
expect(generateResourceSpy).toHaveBeenCalled();
|
|
331
311
|
});
|
|
332
312
|
|
|
333
|
-
// VERIFY: API called exactly once
|
|
313
|
+
// VERIFY: API called exactly once
|
|
334
314
|
expect(generateResourceSpy).toHaveBeenCalledTimes(1);
|
|
335
|
-
|
|
336
|
-
// VERIFY: Our additional listener was called (events work)
|
|
337
|
-
expect(additionalListener).toHaveBeenCalledTimes(1);
|
|
338
|
-
|
|
339
|
-
subscription.unsubscribe();
|
|
340
315
|
});
|
|
341
316
|
|
|
342
317
|
it('should forward final chunk as progress before emitting complete', async () => {
|
|
@@ -385,15 +360,13 @@ describe('Generation Flow - Feature Integration', () => {
|
|
|
385
360
|
function renderYieldFlow(
|
|
386
361
|
testResourceId: ResourceId
|
|
387
362
|
) {
|
|
388
|
-
let eventBusInstance:
|
|
363
|
+
let eventBusInstance: ReturnType<typeof useEventBus>;
|
|
364
|
+
let generateFn: ReturnType<typeof useYieldFlow>['onGenerateDocument'];
|
|
389
365
|
|
|
390
366
|
// Component to capture EventBus instance and set up event operations
|
|
391
367
|
function EventBusCapture() {
|
|
392
368
|
eventBusInstance = useEventBus();
|
|
393
|
-
|
|
394
|
-
// Set up resolution flow (resolve:update-body, resolve:link)
|
|
395
369
|
useBindFlow(testResourceId);
|
|
396
|
-
|
|
397
370
|
return null;
|
|
398
371
|
}
|
|
399
372
|
|
|
@@ -402,12 +375,15 @@ function renderYieldFlow(
|
|
|
402
375
|
const {
|
|
403
376
|
isGenerating,
|
|
404
377
|
generationProgress,
|
|
378
|
+
onGenerateDocument,
|
|
405
379
|
} = useYieldFlow(
|
|
406
380
|
'en',
|
|
407
381
|
testResourceId,
|
|
408
382
|
vi.fn()
|
|
409
383
|
);
|
|
410
384
|
|
|
385
|
+
generateFn = onGenerateDocument;
|
|
386
|
+
|
|
411
387
|
return (
|
|
412
388
|
<div>
|
|
413
389
|
<div data-testid="is-generating">
|
|
@@ -434,9 +410,10 @@ function renderYieldFlow(
|
|
|
434
410
|
return {
|
|
435
411
|
emitGenerationStart: (
|
|
436
412
|
aId: AnnotationId,
|
|
437
|
-
|
|
413
|
+
_rId: ResourceId,
|
|
438
414
|
options: {
|
|
439
415
|
title: string;
|
|
416
|
+
storageUri?: string;
|
|
440
417
|
prompt?: string;
|
|
441
418
|
language?: string;
|
|
442
419
|
temperature?: number;
|
|
@@ -444,11 +421,8 @@ function renderYieldFlow(
|
|
|
444
421
|
context: any;
|
|
445
422
|
}
|
|
446
423
|
) => {
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
resourceId: rId,
|
|
450
|
-
options,
|
|
451
|
-
});
|
|
424
|
+
// Call the hook's callback directly (no longer EventBus-driven)
|
|
425
|
+
generateFn(aId as string, { storageUri: options.storageUri ?? 'file:///tmp/test', ...options });
|
|
452
426
|
},
|
|
453
427
|
getEventBus: () => eventBusInstance,
|
|
454
428
|
};
|
|
@@ -31,7 +31,7 @@ import { useMarkFlow } from '../../../hooks/useMarkFlow';
|
|
|
31
31
|
import { EventBusProvider, useEventBus } from '../../../contexts/EventBusContext';
|
|
32
32
|
import { ApiClientProvider } from '../../../contexts/ApiClientContext';
|
|
33
33
|
import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
|
|
34
|
-
import {
|
|
34
|
+
import { SemiontApiClient } from '@semiont/api-client';
|
|
35
35
|
import type { components } from '@semiont/core';
|
|
36
36
|
|
|
37
37
|
// Mock Toast module to prevent "useToast must be used within a ToastProvider" errors
|
|
@@ -167,11 +167,11 @@ describe('Detection Progress Flow Integration (Layer 3)', () => {
|
|
|
167
167
|
// Create fresh stream for each test
|
|
168
168
|
mockStream = new MockSSEStream();
|
|
169
169
|
|
|
170
|
-
// Spy on
|
|
171
|
-
vi.spyOn(
|
|
172
|
-
vi.spyOn(
|
|
173
|
-
vi.spyOn(
|
|
174
|
-
vi.spyOn(
|
|
170
|
+
// Spy on SemiontApiClient prototype HTTP methods (namespace methods call these)
|
|
171
|
+
vi.spyOn(SemiontApiClient.prototype, 'annotateHighlights').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
|
|
172
|
+
vi.spyOn(SemiontApiClient.prototype, 'annotateAssessments').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
|
|
173
|
+
vi.spyOn(SemiontApiClient.prototype, 'annotateComments').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
|
|
174
|
+
vi.spyOn(SemiontApiClient.prototype, 'annotateReferences').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
|
|
175
175
|
|
|
176
176
|
mockAnnotations = [];
|
|
177
177
|
});
|