@semiont/react-ui 0.2.33-build.79 → 0.2.33-build.80
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-7GvDyO0d.d.mts +414 -0
- package/dist/{PdfAnnotationCanvas.client-ADC4FFSE.mjs → PdfAnnotationCanvas.client-RAJRPQLU.mjs} +42 -27
- package/dist/PdfAnnotationCanvas.client-RAJRPQLU.mjs.map +1 -0
- package/dist/{ar-EMHEHPCJ.mjs → ar-4ZEORRW2.mjs} +7 -4
- package/dist/ar-4ZEORRW2.mjs.map +1 -0
- package/dist/{bn-OVCI4F6X.mjs → bn-SEDE5BQJ.mjs} +7 -4
- package/dist/bn-SEDE5BQJ.mjs.map +1 -0
- package/dist/{chunk-LIHZTECW.mjs → chunk-D7NBW4RV.mjs} +7 -4
- package/dist/chunk-D7NBW4RV.mjs.map +1 -0
- package/dist/{chunk-JZIO2A3B.mjs → chunk-ZR4ZV2LY.mjs} +206 -146
- package/dist/chunk-ZR4ZV2LY.mjs.map +1 -0
- package/dist/{cs-FAN66Q2F.mjs → cs-7W4WF5WD.mjs} +7 -4
- package/dist/cs-7W4WF5WD.mjs.map +1 -0
- package/dist/{da-YBBIHI2O.mjs → da-75XGBCBK.mjs} +7 -4
- package/dist/da-75XGBCBK.mjs.map +1 -0
- package/dist/{de-MAYU33LB.mjs → de-ODJVFLHM.mjs} +7 -4
- package/dist/de-ODJVFLHM.mjs.map +1 -0
- package/dist/{el-MKGSWN4O.mjs → el-C4PM4WB3.mjs} +7 -4
- package/dist/el-C4PM4WB3.mjs.map +1 -0
- package/dist/{en-DDLIXJCU.mjs → en-KJCJQ4OO.mjs} +2 -2
- package/dist/{es-52LHUWJD.mjs → es-WD33R7QL.mjs} +7 -4
- package/dist/es-WD33R7QL.mjs.map +1 -0
- package/dist/{fa-FJICRANB.mjs → fa-2BP6V56P.mjs} +7 -4
- package/dist/fa-2BP6V56P.mjs.map +1 -0
- package/dist/{fi-O455XFCR.mjs → fi-USRRW24J.mjs} +7 -4
- package/dist/fi-USRRW24J.mjs.map +1 -0
- package/dist/{fr-TXIXHOOE.mjs → fr-EC5S6WVF.mjs} +7 -4
- package/dist/fr-EC5S6WVF.mjs.map +1 -0
- package/dist/{he-JBSOX5IN.mjs → he-7TBVIKAA.mjs} +7 -4
- package/dist/he-7TBVIKAA.mjs.map +1 -0
- package/dist/{hi-KGHI3XVT.mjs → hi-FO4VIZLA.mjs} +7 -4
- package/dist/hi-FO4VIZLA.mjs.map +1 -0
- package/dist/{id-5OCPPZLO.mjs → id-7U7GGVWY.mjs} +7 -4
- package/dist/id-7U7GGVWY.mjs.map +1 -0
- package/dist/index.css +123 -85
- package/dist/index.css.map +1 -1
- package/dist/index.d.mts +645 -471
- package/dist/index.mjs +3461 -3025
- package/dist/index.mjs.map +1 -1
- package/dist/{it-PNBBZSM2.mjs → it-Y4OPL6I2.mjs} +7 -4
- package/dist/it-Y4OPL6I2.mjs.map +1 -0
- package/dist/{ja-LDD7R3TJ.mjs → ja-PK7SQL55.mjs} +7 -4
- package/dist/ja-PK7SQL55.mjs.map +1 -0
- package/dist/{ko-F47ZDEY3.mjs → ko-L25PXMYD.mjs} +7 -4
- package/dist/ko-L25PXMYD.mjs.map +1 -0
- package/dist/{ms-Z7LMXJWL.mjs → ms-STH777QM.mjs} +7 -4
- package/dist/ms-STH777QM.mjs.map +1 -0
- package/dist/{nl-6SJFBPJ3.mjs → nl-Y7LECDDR.mjs} +7 -4
- package/dist/nl-Y7LECDDR.mjs.map +1 -0
- package/dist/{no-YXPBPSGF.mjs → no-KEKCEWU6.mjs} +7 -4
- package/dist/no-KEKCEWU6.mjs.map +1 -0
- package/dist/{pl-P4AZ2QME.mjs → pl-7A7OC75O.mjs} +7 -4
- package/dist/pl-7A7OC75O.mjs.map +1 -0
- package/dist/{pt-LHWUS6U6.mjs → pt-35HTM7RA.mjs} +7 -4
- package/dist/pt-35HTM7RA.mjs.map +1 -0
- package/dist/{ro-EA5J2ZON.mjs → ro-VAWL5KQA.mjs} +7 -4
- package/dist/ro-VAWL5KQA.mjs.map +1 -0
- package/dist/{sv-DATBS3UQ.mjs → sv-7ZK5EQEB.mjs} +7 -4
- package/dist/sv-7ZK5EQEB.mjs.map +1 -0
- package/dist/test-utils.d.mts +18 -8
- package/dist/test-utils.mjs +36 -14
- package/dist/test-utils.mjs.map +1 -1
- package/dist/{th-WTFJRWPT.mjs → th-UDWZ4X34.mjs} +7 -4
- package/dist/th-UDWZ4X34.mjs.map +1 -0
- package/dist/{tr-IKO3RXOX.mjs → tr-4WMPK3UX.mjs} +7 -4
- package/dist/tr-4WMPK3UX.mjs.map +1 -0
- package/dist/{uk-CF6CTTRK.mjs → uk-SSLASQYJ.mjs} +7 -4
- package/dist/uk-SSLASQYJ.mjs.map +1 -0
- package/dist/{vi-AJLTXPZQ.mjs → vi-IF42Z5PU.mjs} +7 -4
- package/dist/vi-IF42Z5PU.mjs.map +1 -0
- package/dist/{zh-U3ORHHYH.mjs → zh-HRQTNTAI.mjs} +7 -4
- package/dist/zh-HRQTNTAI.mjs.map +1 -0
- package/package.json +3 -1
- package/src/components/CodeMirrorRenderer.tsx +66 -93
- package/src/components/DetectionProgressWidget.tsx +16 -5
- package/src/components/LiveRegion.tsx +18 -18
- package/src/components/ResizeHandle.tsx +10 -4
- package/src/components/SessionTimer.tsx +2 -2
- package/src/components/Toolbar.tsx +18 -9
- package/src/components/__tests__/SessionTimer.test.tsx +9 -9
- package/src/components/annotation/AnnotateToolbar.tsx +17 -15
- package/src/components/annotation/__tests__/AnnotateToolbar.test.tsx +165 -63
- package/src/components/annotation/annotation-entries.css +10 -0
- package/src/components/annotation-popups/JsonLdView.tsx +8 -2
- package/src/components/image-annotation/AnnotationOverlay.tsx +42 -22
- package/src/components/image-annotation/SvgDrawingCanvas.tsx +27 -30
- package/src/components/layout/__tests__/LeftSidebar.test.tsx +12 -33
- package/src/components/layout/__tests__/PageLayout.test.tsx +37 -32
- package/src/components/layout/__tests__/UnifiedHeader.test.tsx +21 -40
- package/src/components/modals/ResourceSearchModal.tsx +2 -2
- package/src/components/modals/SearchModal.tsx +1 -1
- package/src/components/navigation/CollapsibleResourceNavigation.tsx +14 -9
- package/src/components/navigation/NavigationTabs.css +36 -24
- package/src/components/navigation/ObservableLink.tsx +91 -0
- package/src/components/navigation/SimpleNavigation.tsx +20 -16
- package/src/components/navigation/SortableResourceTab.tsx +11 -5
- package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +51 -26
- package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +28 -22
- package/src/components/resource/AnnotateView.tsx +64 -134
- package/src/components/resource/BrowseView.tsx +86 -166
- package/src/components/resource/HistoryEvent.tsx +13 -7
- package/src/components/resource/ResourceViewer.tsx +122 -264
- package/src/components/resource/__tests__/BrowseView.test.tsx +631 -0
- package/src/components/resource/__tests__/ResourceViewer.mode-switch.test.tsx +231 -0
- package/src/components/resource/panels/AssessmentEntry.tsx +25 -33
- package/src/components/resource/panels/AssessmentPanel.tsx +106 -28
- package/src/components/resource/panels/CommentEntry.tsx +38 -32
- package/src/components/resource/panels/CommentsPanel.tsx +121 -28
- package/src/components/resource/panels/DetectSection.css +36 -1
- package/src/components/resource/panels/DetectSection.tsx +38 -10
- package/src/components/resource/panels/HighlightEntry.tsx +25 -33
- package/src/components/resource/panels/HighlightPanel.tsx +100 -25
- package/src/components/resource/panels/ReferenceEntry.tsx +61 -75
- package/src/components/resource/panels/ReferencesPanel.tsx +134 -42
- package/src/components/resource/panels/ResourceInfoPanel.tsx +47 -48
- package/src/components/resource/panels/TagEntry.tsx +25 -33
- package/src/components/resource/panels/TaggingPanel.tsx +119 -30
- package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +30 -92
- package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +129 -110
- package/src/components/resource/panels/__tests__/CommentEntry.test.tsx +86 -78
- package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +144 -149
- package/src/components/resource/panels/__tests__/DetectSection.test.tsx +480 -0
- package/src/components/resource/panels/__tests__/HighlightPanel.detectionProgress.test.tsx +362 -0
- package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +226 -111
- package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +117 -61
- package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +128 -106
- package/src/components/settings/SettingsPanel.tsx +15 -12
- package/src/features/admin-devops/__tests__/AdminDevOpsPage.test.tsx +1 -46
- package/src/features/admin-devops/components/AdminDevOpsPage.tsx +0 -9
- package/src/features/admin-security/__tests__/AdminSecurityPage.test.tsx +0 -3
- package/src/features/admin-security/components/AdminSecurityPage.tsx +0 -9
- package/src/features/admin-users/__tests__/AdminUsersPage.test.tsx +0 -3
- package/src/features/admin-users/components/AdminUsersPage.tsx +0 -9
- package/src/features/moderate-entity-tags/__tests__/EntityTagsPage.test.tsx +0 -3
- package/src/features/moderate-entity-tags/components/EntityTagsPage.tsx +1 -9
- package/src/features/moderate-recent/__tests__/RecentDocumentsPage.test.tsx +0 -32
- package/src/features/moderate-recent/components/RecentDocumentsPage.tsx +1 -9
- package/src/features/moderate-tag-schemas/__tests__/TagSchemasPage.test.tsx +0 -32
- package/src/features/moderate-tag-schemas/components/TagSchemasPage.tsx +1 -9
- package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +51 -54
- package/src/features/resource-compose/components/ResourceComposePage.tsx +3 -13
- package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +39 -45
- package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +9 -13
- package/src/features/resource-viewer/__tests__/AnnotationDeletionIntegration.test.tsx +231 -0
- package/src/features/resource-viewer/__tests__/DetectionFlowBug.test.tsx +234 -0
- package/src/features/resource-viewer/__tests__/DetectionFlowIntegration.test.tsx +388 -0
- package/src/features/resource-viewer/__tests__/DetectionProgressDismissal.test.tsx +318 -0
- package/src/features/resource-viewer/__tests__/GenerationFlowIntegration.test.tsx +504 -0
- package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +135 -88
- package/src/features/resource-viewer/__tests__/detection-progress-flow.test.tsx +322 -0
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +308 -528
- package/translations/ar.json +6 -3
- package/translations/bn.json +6 -3
- package/translations/cs.json +6 -3
- package/translations/da.json +6 -3
- package/translations/de.json +6 -3
- package/translations/el.json +6 -3
- package/translations/en.json +6 -3
- package/translations/es.json +6 -3
- package/translations/fa.json +6 -3
- package/translations/fi.json +6 -3
- package/translations/fr.json +6 -3
- package/translations/he.json +6 -3
- package/translations/hi.json +6 -3
- package/translations/id.json +6 -3
- package/translations/it.json +6 -3
- package/translations/ja.json +6 -3
- package/translations/ko.json +6 -3
- package/translations/ms.json +6 -3
- package/translations/nl.json +6 -3
- package/translations/no.json +6 -3
- package/translations/pl.json +6 -3
- package/translations/pt.json +6 -3
- package/translations/ro.json +6 -3
- package/translations/sv.json +6 -3
- package/translations/th.json +6 -3
- package/translations/tr.json +6 -3
- package/translations/uk.json +6 -3
- package/translations/vi.json +6 -3
- package/translations/zh.json +6 -3
- package/dist/PdfAnnotationCanvas.client-ADC4FFSE.mjs.map +0 -1
- package/dist/TranslationManager-Co_5fSxl.d.mts +0 -118
- package/dist/ar-EMHEHPCJ.mjs.map +0 -1
- package/dist/bn-OVCI4F6X.mjs.map +0 -1
- package/dist/chunk-JZIO2A3B.mjs.map +0 -1
- package/dist/chunk-LIHZTECW.mjs.map +0 -1
- package/dist/cs-FAN66Q2F.mjs.map +0 -1
- package/dist/da-YBBIHI2O.mjs.map +0 -1
- package/dist/de-MAYU33LB.mjs.map +0 -1
- package/dist/el-MKGSWN4O.mjs.map +0 -1
- package/dist/es-52LHUWJD.mjs.map +0 -1
- package/dist/fa-FJICRANB.mjs.map +0 -1
- package/dist/fi-O455XFCR.mjs.map +0 -1
- package/dist/fr-TXIXHOOE.mjs.map +0 -1
- package/dist/he-JBSOX5IN.mjs.map +0 -1
- package/dist/hi-KGHI3XVT.mjs.map +0 -1
- package/dist/id-5OCPPZLO.mjs.map +0 -1
- package/dist/it-PNBBZSM2.mjs.map +0 -1
- package/dist/ja-LDD7R3TJ.mjs.map +0 -1
- package/dist/ko-F47ZDEY3.mjs.map +0 -1
- package/dist/ms-Z7LMXJWL.mjs.map +0 -1
- package/dist/nl-6SJFBPJ3.mjs.map +0 -1
- package/dist/no-YXPBPSGF.mjs.map +0 -1
- package/dist/pl-P4AZ2QME.mjs.map +0 -1
- package/dist/pt-LHWUS6U6.mjs.map +0 -1
- package/dist/ro-EA5J2ZON.mjs.map +0 -1
- package/dist/sv-DATBS3UQ.mjs.map +0 -1
- package/dist/th-WTFJRWPT.mjs.map +0 -1
- package/dist/tr-IKO3RXOX.mjs.map +0 -1
- package/dist/uk-CF6CTTRK.mjs.map +0 -1
- package/dist/vi-AJLTXPZQ.mjs.map +0 -1
- package/dist/zh-U3ORHHYH.mjs.map +0 -1
- /package/dist/{en-DDLIXJCU.mjs.map → en-KJCJQ4OO.mjs.map} +0 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FAILING TEST: Reproduces the bug where detection events fire but state doesn't update
|
|
3
|
+
*
|
|
4
|
+
* Based on console logs from production:
|
|
5
|
+
* ✅ detection:start emitted
|
|
6
|
+
* ✅ detection:progress emitted
|
|
7
|
+
* ❌ detectingMotivation remains null
|
|
8
|
+
* ❌ detectionProgress remains null
|
|
9
|
+
*
|
|
10
|
+
* UPDATED: Now tests useDetectionFlow hook instead of DetectionFlowContainer
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
14
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
15
|
+
import { act } from 'react';
|
|
16
|
+
import { useDetectionFlow } from '../../../hooks/useDetectionFlow';
|
|
17
|
+
import { EventBusProvider, useEventBus, resetEventBusForTesting } from '../../../contexts/EventBusContext';
|
|
18
|
+
import { ApiClientProvider } from '../../../contexts/ApiClientContext';
|
|
19
|
+
import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
|
|
20
|
+
import { SSEClient } from '@semiont/api-client';
|
|
21
|
+
|
|
22
|
+
describe('REPRODUCING BUG: Detection state not updating', () => {
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
resetEventBusForTesting();
|
|
25
|
+
vi.clearAllMocks();
|
|
26
|
+
|
|
27
|
+
// Minimal mock - SSE streams not needed for this test
|
|
28
|
+
vi.spyOn(SSEClient.prototype, 'detectAnnotations').mockReturnValue({ onProgress: vi.fn().mockReturnThis(), onComplete: vi.fn().mockReturnThis(), onError: vi.fn().mockReturnThis(), close: vi.fn() } as any);
|
|
29
|
+
vi.spyOn(SSEClient.prototype, 'detectHighlights').mockReturnValue({ onProgress: vi.fn().mockReturnThis(), onComplete: vi.fn().mockReturnThis(), onError: vi.fn().mockReturnThis(), close: vi.fn() } as any);
|
|
30
|
+
vi.spyOn(SSEClient.prototype, 'detectComments').mockReturnValue({ onProgress: vi.fn().mockReturnThis(), onComplete: vi.fn().mockReturnThis(), onError: vi.fn().mockReturnThis(), close: vi.fn() } as any);
|
|
31
|
+
vi.spyOn(SSEClient.prototype, 'detectAssessments').mockReturnValue({ onProgress: vi.fn().mockReturnThis(), onComplete: vi.fn().mockReturnThis(), onError: vi.fn().mockReturnThis(), close: vi.fn() } as any);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
vi.restoreAllMocks();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('SHOULD update state when detection:start event is emitted', async () => {
|
|
39
|
+
let eventBusInstance: any;
|
|
40
|
+
let currentState: any;
|
|
41
|
+
|
|
42
|
+
// Component to capture EventBus and hook state
|
|
43
|
+
function TestComponent() {
|
|
44
|
+
eventBusInstance = useEventBus();
|
|
45
|
+
const state = useDetectionFlow('http://localhost:8080/resources/test' as any);
|
|
46
|
+
currentState = state;
|
|
47
|
+
|
|
48
|
+
console.log('[TEST] useDetectionFlow state:', {
|
|
49
|
+
detectingMotivation: state.detectingMotivation,
|
|
50
|
+
detectionProgress: state.detectionProgress,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div>
|
|
55
|
+
<div data-testid="detecting">{state.detectingMotivation || 'null'}</div>
|
|
56
|
+
<div data-testid="progress">{state.detectionProgress?.message || 'null'}</div>
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
render(
|
|
62
|
+
<EventBusProvider>
|
|
63
|
+
<AuthTokenProvider token={null}>
|
|
64
|
+
<ApiClientProvider baseUrl="http://localhost:4000">
|
|
65
|
+
<TestComponent />
|
|
66
|
+
</ApiClientProvider>
|
|
67
|
+
</AuthTokenProvider>
|
|
68
|
+
</EventBusProvider>
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
// Initial state should be null
|
|
72
|
+
expect(screen.getByTestId('detecting')).toHaveTextContent('null');
|
|
73
|
+
expect(screen.getByTestId('progress')).toHaveTextContent('null');
|
|
74
|
+
|
|
75
|
+
console.log('[TEST] Emitting detection:start event...');
|
|
76
|
+
|
|
77
|
+
// Emit detection:start event (exactly like production)
|
|
78
|
+
act(() => {
|
|
79
|
+
eventBusInstance.emit('detection:start', {
|
|
80
|
+
motivation: 'linking',
|
|
81
|
+
options: { entityTypes: ['Location'] }
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
console.log('[TEST] After detection:start, checking state...');
|
|
86
|
+
|
|
87
|
+
// THIS SHOULD PASS but currently FAILS
|
|
88
|
+
await waitFor(() => {
|
|
89
|
+
expect(screen.getByTestId('detecting')).toHaveTextContent('linking');
|
|
90
|
+
}, { timeout: 1000 });
|
|
91
|
+
|
|
92
|
+
expect(currentState.detectingMotivation).toBe('linking');
|
|
93
|
+
expect(currentState.detectionProgress).toBeNull(); // Should clear on start
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('SHOULD update state when detection:progress event is emitted', async () => {
|
|
97
|
+
let eventBusInstance: any;
|
|
98
|
+
let currentState: any;
|
|
99
|
+
|
|
100
|
+
function TestComponent() {
|
|
101
|
+
eventBusInstance = useEventBus();
|
|
102
|
+
const state = useDetectionFlow('http://localhost:8080/resources/test' as any);
|
|
103
|
+
currentState = state;
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<div>
|
|
107
|
+
<div data-testid="detecting">{state.detectingMotivation || 'null'}</div>
|
|
108
|
+
<div data-testid="progress">{state.detectionProgress?.message || 'null'}</div>
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
render(
|
|
114
|
+
<EventBusProvider>
|
|
115
|
+
<AuthTokenProvider token={null}>
|
|
116
|
+
<ApiClientProvider baseUrl="http://localhost:4000">
|
|
117
|
+
<TestComponent />
|
|
118
|
+
</ApiClientProvider>
|
|
119
|
+
</AuthTokenProvider>
|
|
120
|
+
</EventBusProvider>
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
console.log('[TEST] Emitting detection:progress event...');
|
|
124
|
+
|
|
125
|
+
// Emit detection:progress event (exactly like production)
|
|
126
|
+
act(() => {
|
|
127
|
+
eventBusInstance.emit('detection:progress', {
|
|
128
|
+
status: 'started',
|
|
129
|
+
resourceId: 'test',
|
|
130
|
+
totalEntityTypes: 1,
|
|
131
|
+
processedEntityTypes: 0,
|
|
132
|
+
message: 'Starting entity detection...'
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
console.log('[TEST] After detection:progress, checking state...');
|
|
137
|
+
|
|
138
|
+
// THIS SHOULD PASS but currently FAILS
|
|
139
|
+
await waitFor(() => {
|
|
140
|
+
expect(screen.getByTestId('progress')).toHaveTextContent('Starting entity detection...');
|
|
141
|
+
}, { timeout: 1000 });
|
|
142
|
+
|
|
143
|
+
expect(currentState.detectionProgress).toMatchObject({
|
|
144
|
+
status: 'started',
|
|
145
|
+
message: 'Starting entity detection...'
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('SHOULD show EXACTLY the production bug', async () => {
|
|
150
|
+
let eventBusInstance: any;
|
|
151
|
+
const stateSnapshots: any[] = [];
|
|
152
|
+
|
|
153
|
+
function TestComponent() {
|
|
154
|
+
eventBusInstance = useEventBus();
|
|
155
|
+
const state = useDetectionFlow('http://localhost:8080/resources/f45fd44f9cb0b0fe1b7980d3d034bc61' as any);
|
|
156
|
+
|
|
157
|
+
stateSnapshots.push({
|
|
158
|
+
detectingMotivation: state.detectingMotivation,
|
|
159
|
+
detectionProgress: state.detectionProgress,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<div>
|
|
164
|
+
<div data-testid="detecting">{state.detectingMotivation || 'null'}</div>
|
|
165
|
+
<div data-testid="progress">{state.detectionProgress?.message || 'null'}</div>
|
|
166
|
+
</div>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
render(
|
|
171
|
+
<EventBusProvider>
|
|
172
|
+
<AuthTokenProvider token={null}>
|
|
173
|
+
<ApiClientProvider baseUrl="http://localhost:4000">
|
|
174
|
+
<TestComponent />
|
|
175
|
+
</ApiClientProvider>
|
|
176
|
+
</AuthTokenProvider>
|
|
177
|
+
</EventBusProvider>
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
console.log('\n=== REPRODUCING PRODUCTION BUG ===');
|
|
181
|
+
console.log('Initial state:', stateSnapshots[stateSnapshots.length - 1]);
|
|
182
|
+
|
|
183
|
+
// Exactly like production logs
|
|
184
|
+
act(() => {
|
|
185
|
+
console.log('[EventBus] emit: detection:start {motivation: "linking", options: {...}}');
|
|
186
|
+
eventBusInstance.emit('detection:start', {
|
|
187
|
+
motivation: 'linking',
|
|
188
|
+
options: { entityTypes: ['Location'] }
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
console.log('After detection:start:', stateSnapshots[stateSnapshots.length - 1]);
|
|
193
|
+
|
|
194
|
+
act(() => {
|
|
195
|
+
console.log('[EventBus] emit: detection:progress {status: "started", ...}');
|
|
196
|
+
eventBusInstance.emit('detection:progress', {
|
|
197
|
+
status: 'started',
|
|
198
|
+
resourceId: 'f45fd44f9cb0b0fe1b7980d3d034bc61',
|
|
199
|
+
totalEntityTypes: 1,
|
|
200
|
+
processedEntityTypes: 0,
|
|
201
|
+
message: 'Starting entity detection...'
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
console.log('After detection:progress:', stateSnapshots[stateSnapshots.length - 1]);
|
|
206
|
+
|
|
207
|
+
act(() => {
|
|
208
|
+
console.log('[EventBus] emit: detection:progress {status: "scanning", ...}');
|
|
209
|
+
eventBusInstance.emit('detection:progress', {
|
|
210
|
+
status: 'scanning',
|
|
211
|
+
resourceId: 'f45fd44f9cb0b0fe1b7980d3d034bc61',
|
|
212
|
+
currentEntityType: 'Location',
|
|
213
|
+
totalEntityTypes: 1,
|
|
214
|
+
processedEntityTypes: 1,
|
|
215
|
+
message: 'Scanning for Location...'
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
console.log('After second detection:progress:', stateSnapshots[stateSnapshots.length - 1]);
|
|
220
|
+
console.log('=== END REPRODUCTION ===\n');
|
|
221
|
+
|
|
222
|
+
// THIS IS THE BUG: Events fire but state never updates
|
|
223
|
+
// Production logs show: detectingMotivation: null, detectionProgress: null
|
|
224
|
+
// Even though events were emitted
|
|
225
|
+
await waitFor(() => {
|
|
226
|
+
const currentSnapshot = stateSnapshots[stateSnapshots.length - 1];
|
|
227
|
+
console.log('Final state check:', currentSnapshot);
|
|
228
|
+
|
|
229
|
+
// These SHOULD pass but will FAIL if bug is present
|
|
230
|
+
expect(currentSnapshot.detectingMotivation).toBe('linking');
|
|
231
|
+
expect(currentSnapshot.detectionProgress?.message).toBe('Scanning for Location...');
|
|
232
|
+
}, { timeout: 2000 });
|
|
233
|
+
});
|
|
234
|
+
});
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer 3: Feature Integration Test - Detection Flow Architecture
|
|
3
|
+
*
|
|
4
|
+
* Tests the COMPLETE detection flow with real component composition:
|
|
5
|
+
* - EventBusProvider (REAL)
|
|
6
|
+
* - ApiClientProvider (REAL, with MOCKED client)
|
|
7
|
+
* - useDetectionFlow (REAL)
|
|
8
|
+
* - useEventOperations (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 different motivations call correct API methods
|
|
15
|
+
* - Ensures multiple event listeners don't cause duplicate API calls
|
|
16
|
+
*
|
|
17
|
+
* COMPLEMENTARY TEST: See detection-progress-flow.test.tsx for UI/UX testing
|
|
18
|
+
* - That test verifies the USER EXPERIENCE (button clicks, progress display)
|
|
19
|
+
* - This test verifies the SYSTEM ARCHITECTURE (event wiring, API calls)
|
|
20
|
+
*
|
|
21
|
+
* NO BACKEND SERVER - only mocked API client boundary
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import React from 'react';
|
|
25
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
26
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
27
|
+
import { act } from 'react';
|
|
28
|
+
import { useDetectionFlow } from '../../../hooks/useDetectionFlow';
|
|
29
|
+
import { EventBusProvider, useEventBus, resetEventBusForTesting } from '../../../contexts/EventBusContext';
|
|
30
|
+
import { ApiClientProvider } from '../../../contexts/ApiClientContext';
|
|
31
|
+
import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
|
|
32
|
+
import { SSEClient } from '@semiont/api-client';
|
|
33
|
+
import type { Motivation } from '@semiont/api-client';
|
|
34
|
+
import { resourceUri } from '@semiont/api-client';
|
|
35
|
+
import type { Emitter } from 'mitt';
|
|
36
|
+
import type { EventMap } from '../../../contexts/EventBusContext';
|
|
37
|
+
|
|
38
|
+
// Mock SSE stream that we can control in tests
|
|
39
|
+
const createMockSSEStream = () => {
|
|
40
|
+
const stream = {
|
|
41
|
+
onProgressCallback: null as ((chunk: any) => void) | null,
|
|
42
|
+
onCompleteCallback: null as (() => void) | null,
|
|
43
|
+
onErrorCallback: null as ((error: Error) => void) | null,
|
|
44
|
+
onProgress: vi.fn((callback: (chunk: any) => void) => {
|
|
45
|
+
stream.onProgressCallback = callback;
|
|
46
|
+
return stream;
|
|
47
|
+
}),
|
|
48
|
+
onComplete: vi.fn((callback: () => void) => {
|
|
49
|
+
stream.onCompleteCallback = callback;
|
|
50
|
+
return stream;
|
|
51
|
+
}),
|
|
52
|
+
onError: vi.fn((callback: (error: Error) => void) => {
|
|
53
|
+
stream.onErrorCallback = callback;
|
|
54
|
+
return stream;
|
|
55
|
+
}),
|
|
56
|
+
close: vi.fn(),
|
|
57
|
+
};
|
|
58
|
+
return stream;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
describe('Detection Flow - Feature Integration', () => {
|
|
62
|
+
let mockStream: ReturnType<typeof createMockSSEStream>;
|
|
63
|
+
let detectAnnotationsSpy: any;
|
|
64
|
+
let detectHighlightsSpy: any;
|
|
65
|
+
let detectCommentsSpy: any;
|
|
66
|
+
|
|
67
|
+
beforeEach(() => {
|
|
68
|
+
vi.clearAllMocks();
|
|
69
|
+
resetEventBusForTesting();
|
|
70
|
+
|
|
71
|
+
// Create fresh mock stream for each test
|
|
72
|
+
mockStream = createMockSSEStream();
|
|
73
|
+
|
|
74
|
+
// Spy on SSEClient prototype methods
|
|
75
|
+
detectAnnotationsSpy = vi.spyOn(SSEClient.prototype, 'detectAnnotations').mockReturnValue(mockStream as any);
|
|
76
|
+
detectHighlightsSpy = vi.spyOn(SSEClient.prototype, 'detectHighlights').mockReturnValue(mockStream as any);
|
|
77
|
+
detectCommentsSpy = vi.spyOn(SSEClient.prototype, 'detectComments').mockReturnValue(mockStream as any);
|
|
78
|
+
vi.spyOn(SSEClient.prototype, 'detectAssessments').mockReturnValue(mockStream as any);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
afterEach(() => {
|
|
82
|
+
vi.restoreAllMocks();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should call detectAnnotations exactly ONCE when detection starts (not twice)', async () => {
|
|
86
|
+
const testUri = resourceUri('http://localhost:4000/resources/test-resource');
|
|
87
|
+
|
|
88
|
+
// Render with real component composition
|
|
89
|
+
const { emitDetectionStart } = renderDetectionFlow(testUri);
|
|
90
|
+
|
|
91
|
+
// Trigger detection for linking (uses detectAnnotations)
|
|
92
|
+
act(() => {
|
|
93
|
+
emitDetectionStart('linking', {
|
|
94
|
+
entityTypes: ['Person', 'Organization'],
|
|
95
|
+
includeDescriptiveReferences: false
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// CRITICAL ASSERTION: API called exactly once (not twice!)
|
|
100
|
+
// This would FAIL if useEventOperations was called in multiple places
|
|
101
|
+
await waitFor(() => {
|
|
102
|
+
expect(detectAnnotationsSpy).toHaveBeenCalledTimes(1);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Verify correct parameters
|
|
106
|
+
expect(detectAnnotationsSpy).toHaveBeenCalledWith(
|
|
107
|
+
testUri,
|
|
108
|
+
{
|
|
109
|
+
entityTypes: ['Person', 'Organization'],
|
|
110
|
+
includeDescriptiveReferences: false,
|
|
111
|
+
},
|
|
112
|
+
{ auth: undefined }
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should propagate SSE progress events to useDetectionFlow state', async () => {
|
|
117
|
+
const testUri = resourceUri('http://localhost:4000/resources/test-resource');
|
|
118
|
+
|
|
119
|
+
// Render with state observer
|
|
120
|
+
const { emitDetectionStart } = renderDetectionFlow(testUri);
|
|
121
|
+
|
|
122
|
+
// Start detection
|
|
123
|
+
act(() => {
|
|
124
|
+
emitDetectionStart('linking', {
|
|
125
|
+
entityTypes: ['Person']
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Wait for stream to be created
|
|
130
|
+
await waitFor(() => {
|
|
131
|
+
expect(detectAnnotationsSpy).toHaveBeenCalled();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Simulate SSE progress callback being invoked
|
|
135
|
+
act(() => {
|
|
136
|
+
mockStream.onProgressCallback!({
|
|
137
|
+
status: 'scanning',
|
|
138
|
+
message: 'Scanning for Person...',
|
|
139
|
+
currentEntityType: 'Person',
|
|
140
|
+
totalEntityTypes: 1,
|
|
141
|
+
processedEntityTypes: 0,
|
|
142
|
+
foundCount: 5,
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Verify progress propagated to UI
|
|
147
|
+
await waitFor(() => {
|
|
148
|
+
expect(screen.getByTestId('progress')).toHaveTextContent('Scanning for Person...');
|
|
149
|
+
expect(screen.getByTestId('detecting')).toHaveTextContent('linking');
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should handle multiple progress updates correctly', async () => {
|
|
154
|
+
const testUri = resourceUri('http://localhost:4000/resources/test-resource');
|
|
155
|
+
const { emitDetectionStart } = renderDetectionFlow(testUri);
|
|
156
|
+
|
|
157
|
+
// Start detection
|
|
158
|
+
act(() => {
|
|
159
|
+
emitDetectionStart('highlighting', {
|
|
160
|
+
instructions: 'Find important passages'
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
await waitFor(() => {
|
|
165
|
+
expect(detectHighlightsSpy).toHaveBeenCalledTimes(1);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// First progress update
|
|
169
|
+
act(() => {
|
|
170
|
+
mockStream.onProgressCallback!({
|
|
171
|
+
status: 'started',
|
|
172
|
+
message: 'Starting analysis...',
|
|
173
|
+
percentage: 0,
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
await waitFor(() => {
|
|
178
|
+
expect(screen.getByTestId('progress')).toHaveTextContent('Starting analysis...');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Second progress update
|
|
182
|
+
act(() => {
|
|
183
|
+
mockStream.onProgressCallback!({
|
|
184
|
+
status: 'analyzing',
|
|
185
|
+
message: 'Analyzing text...',
|
|
186
|
+
percentage: 50,
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
await waitFor(() => {
|
|
191
|
+
expect(screen.getByTestId('progress')).toHaveTextContent('Analyzing text...');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Final progress update
|
|
195
|
+
act(() => {
|
|
196
|
+
mockStream.onProgressCallback!({
|
|
197
|
+
status: 'complete',
|
|
198
|
+
message: 'Created 14 highlights',
|
|
199
|
+
percentage: 100,
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
await waitFor(() => {
|
|
204
|
+
expect(screen.getByTestId('progress')).toHaveTextContent('Created 14 highlights');
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('should keep progress visible after detection completes', async () => {
|
|
209
|
+
const testUri = resourceUri('http://localhost:4000/resources/test-resource');
|
|
210
|
+
const { emitDetectionStart, getEventBus } = renderDetectionFlow(testUri);
|
|
211
|
+
|
|
212
|
+
// Start detection
|
|
213
|
+
act(() => {
|
|
214
|
+
emitDetectionStart('highlighting', { instructions: 'Test' });
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
await waitFor(() => {
|
|
218
|
+
expect(screen.getByTestId('detecting')).toHaveTextContent('highlighting');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Send final progress
|
|
222
|
+
act(() => {
|
|
223
|
+
mockStream.onProgressCallback!({
|
|
224
|
+
status: 'complete',
|
|
225
|
+
message: 'Created 14 highlights',
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
await waitFor(() => {
|
|
230
|
+
expect(screen.getByTestId('progress')).toHaveTextContent('Created 14 highlights');
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// Emit completion event
|
|
234
|
+
act(() => {
|
|
235
|
+
getEventBus().emit('detection:complete', { motivation: 'highlighting' });
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// Verify: detecting flag cleared BUT progress still visible
|
|
239
|
+
await waitFor(() => {
|
|
240
|
+
expect(screen.getByTestId('detecting')).toHaveTextContent('none');
|
|
241
|
+
expect(screen.getByTestId('progress')).toHaveTextContent('Created 14 highlights');
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('should clear progress on detection failure', async () => {
|
|
246
|
+
const testUri = resourceUri('http://localhost:4000/resources/test-resource');
|
|
247
|
+
const { emitDetectionStart, getEventBus } = renderDetectionFlow(testUri);
|
|
248
|
+
|
|
249
|
+
// Start detection
|
|
250
|
+
act(() => {
|
|
251
|
+
emitDetectionStart('linking', { entityTypes: ['Person'] });
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// Add some progress
|
|
255
|
+
act(() => {
|
|
256
|
+
mockStream.onProgressCallback!({
|
|
257
|
+
status: 'scanning',
|
|
258
|
+
message: 'Scanning...',
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
await waitFor(() => {
|
|
263
|
+
expect(screen.getByTestId('progress')).toHaveTextContent('Scanning...');
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// Emit failure
|
|
267
|
+
act(() => {
|
|
268
|
+
getEventBus().emit('detection:failed', { type: 'job.failed', resourceId: 'test-resource' as any, payload: { jobId: 'job-1' as any, jobType: 'detection', error: 'Network error' } });
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// Verify: both detecting and progress cleared
|
|
272
|
+
await waitFor(() => {
|
|
273
|
+
expect(screen.getByTestId('detecting')).toHaveTextContent('none');
|
|
274
|
+
expect(screen.getByTestId('progress')).toHaveTextContent('No progress');
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('should handle different detection motivations with correct API calls', async () => {
|
|
279
|
+
const testUri = resourceUri('http://localhost:4000/resources/test-resource');
|
|
280
|
+
const { emitDetectionStart } = renderDetectionFlow(testUri);
|
|
281
|
+
|
|
282
|
+
// Test highlighting
|
|
283
|
+
act(() => {
|
|
284
|
+
emitDetectionStart('highlighting', { instructions: 'Find important text' });
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
await waitFor(() => {
|
|
288
|
+
expect(detectHighlightsSpy).toHaveBeenCalledTimes(1);
|
|
289
|
+
expect(detectHighlightsSpy).toHaveBeenCalledWith(testUri, {
|
|
290
|
+
instructions: 'Find important text',
|
|
291
|
+
}, { auth: undefined });
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// Reset for next test
|
|
295
|
+
vi.clearAllMocks();
|
|
296
|
+
mockStream = createMockSSEStream();
|
|
297
|
+
detectCommentsSpy.mockReturnValue(mockStream);
|
|
298
|
+
|
|
299
|
+
// Test commenting
|
|
300
|
+
act(() => {
|
|
301
|
+
emitDetectionStart('commenting', {
|
|
302
|
+
instructions: 'Add helpful comments',
|
|
303
|
+
tone: 'educational'
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
await waitFor(() => {
|
|
308
|
+
expect(detectCommentsSpy).toHaveBeenCalledTimes(1);
|
|
309
|
+
expect(detectCommentsSpy).toHaveBeenCalledWith(testUri, {
|
|
310
|
+
instructions: 'Add helpful comments',
|
|
311
|
+
tone: 'educational',
|
|
312
|
+
}, { auth: undefined });
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('should only call API once even with multiple event listeners', async () => {
|
|
317
|
+
const testUri = resourceUri('http://localhost:4000/resources/test-resource');
|
|
318
|
+
|
|
319
|
+
// This test specifically catches the duplicate useEventOperations bug
|
|
320
|
+
// If multiple components call useEventOperations, we'll see multiple API calls
|
|
321
|
+
const { emitDetectionStart, getEventBus } = renderDetectionFlow(testUri);
|
|
322
|
+
|
|
323
|
+
// Add an additional event listener (simulating multiple subscribers)
|
|
324
|
+
const additionalListener = vi.fn();
|
|
325
|
+
getEventBus().on('detection:start', additionalListener);
|
|
326
|
+
|
|
327
|
+
// Trigger detection
|
|
328
|
+
act(() => {
|
|
329
|
+
emitDetectionStart('linking', { entityTypes: ['Person'] });
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// Wait for operation to complete
|
|
333
|
+
await waitFor(() => {
|
|
334
|
+
expect(detectAnnotationsSpy).toHaveBeenCalled();
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// VERIFY: API called exactly once, even though multiple listeners exist
|
|
338
|
+
expect(detectAnnotationsSpy).toHaveBeenCalledTimes(1);
|
|
339
|
+
|
|
340
|
+
// VERIFY: Our additional listener was called (events work)
|
|
341
|
+
expect(additionalListener).toHaveBeenCalledTimes(1);
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Helper: Render useDetectionFlow hook with real component composition
|
|
347
|
+
* Returns methods to interact with the rendered component
|
|
348
|
+
*/
|
|
349
|
+
function renderDetectionFlow(testUri: string) {
|
|
350
|
+
let eventBusInstance: Emitter<EventMap>;
|
|
351
|
+
|
|
352
|
+
// Component to capture EventBus instance
|
|
353
|
+
function EventBusCapture() {
|
|
354
|
+
eventBusInstance = useEventBus();
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Test harness component that uses the hook
|
|
359
|
+
function DetectionFlowTestHarness() {
|
|
360
|
+
const { detectionProgress, detectingMotivation } = useDetectionFlow(testUri as any);
|
|
361
|
+
return (
|
|
362
|
+
<div>
|
|
363
|
+
<div data-testid="detecting">{detectingMotivation || 'none'}</div>
|
|
364
|
+
<div data-testid="progress">
|
|
365
|
+
{detectionProgress?.message || 'No progress'}
|
|
366
|
+
</div>
|
|
367
|
+
</div>
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
render(
|
|
372
|
+
<EventBusProvider>
|
|
373
|
+
<AuthTokenProvider token={null}>
|
|
374
|
+
<ApiClientProvider baseUrl="http://localhost:4000">
|
|
375
|
+
<EventBusCapture />
|
|
376
|
+
<DetectionFlowTestHarness />
|
|
377
|
+
</ApiClientProvider>
|
|
378
|
+
</AuthTokenProvider>
|
|
379
|
+
</EventBusProvider>
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
emitDetectionStart: (motivation: Motivation, options: any) => {
|
|
384
|
+
eventBusInstance.emit('detection:start', { motivation, options });
|
|
385
|
+
},
|
|
386
|
+
getEventBus: () => eventBusInstance,
|
|
387
|
+
};
|
|
388
|
+
}
|