@semiont/react-ui 0.2.33-build.78 → 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-RNNSPLQB.mjs → ar-4ZEORRW2.mjs} +8 -4
- package/dist/ar-4ZEORRW2.mjs.map +1 -0
- package/dist/{bn-S2CDL7EC.mjs → bn-SEDE5BQJ.mjs} +8 -4
- package/dist/bn-SEDE5BQJ.mjs.map +1 -0
- package/dist/{chunk-UDX2Q35T.mjs → chunk-D7NBW4RV.mjs} +8 -4
- package/dist/chunk-D7NBW4RV.mjs.map +1 -0
- package/dist/{chunk-35LLVRFK.mjs → chunk-ZR4ZV2LY.mjs} +206 -146
- package/dist/chunk-ZR4ZV2LY.mjs.map +1 -0
- package/dist/{cs-RSV675WU.mjs → cs-7W4WF5WD.mjs} +8 -4
- package/dist/cs-7W4WF5WD.mjs.map +1 -0
- package/dist/{da-CHXNPWJC.mjs → da-75XGBCBK.mjs} +8 -4
- package/dist/da-75XGBCBK.mjs.map +1 -0
- package/dist/{de-KPEZ53D4.mjs → de-ODJVFLHM.mjs} +8 -4
- package/dist/de-ODJVFLHM.mjs.map +1 -0
- package/dist/{el-MW2BME5T.mjs → el-C4PM4WB3.mjs} +8 -4
- package/dist/el-C4PM4WB3.mjs.map +1 -0
- package/dist/{en-EVMIX24Y.mjs → en-KJCJQ4OO.mjs} +2 -2
- package/dist/{es-HQ24NYS3.mjs → es-WD33R7QL.mjs} +8 -4
- package/dist/es-WD33R7QL.mjs.map +1 -0
- package/dist/{fa-W34LRLHG.mjs → fa-2BP6V56P.mjs} +8 -4
- package/dist/fa-2BP6V56P.mjs.map +1 -0
- package/dist/{fi-3U44IGOA.mjs → fi-USRRW24J.mjs} +8 -4
- package/dist/fi-USRRW24J.mjs.map +1 -0
- package/dist/{fr-N7DKX6NN.mjs → fr-EC5S6WVF.mjs} +8 -4
- package/dist/fr-EC5S6WVF.mjs.map +1 -0
- package/dist/{he-CS4WRXN3.mjs → he-7TBVIKAA.mjs} +8 -4
- package/dist/he-7TBVIKAA.mjs.map +1 -0
- package/dist/{hi-GJDY46KA.mjs → hi-FO4VIZLA.mjs} +8 -4
- package/dist/hi-FO4VIZLA.mjs.map +1 -0
- package/dist/{id-WAEZJK2Y.mjs → id-7U7GGVWY.mjs} +8 -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 +699 -529
- package/dist/index.mjs +4291 -3491
- package/dist/index.mjs.map +1 -1
- package/dist/{it-VDNDMZPU.mjs → it-Y4OPL6I2.mjs} +8 -4
- package/dist/it-Y4OPL6I2.mjs.map +1 -0
- package/dist/{ja-5PEH56J5.mjs → ja-PK7SQL55.mjs} +8 -4
- package/dist/ja-PK7SQL55.mjs.map +1 -0
- package/dist/{ko-JYPL3WVA.mjs → ko-L25PXMYD.mjs} +8 -4
- package/dist/ko-L25PXMYD.mjs.map +1 -0
- package/dist/{ms-5PZVW76T.mjs → ms-STH777QM.mjs} +8 -4
- package/dist/ms-STH777QM.mjs.map +1 -0
- package/dist/{nl-YXES36KM.mjs → nl-Y7LECDDR.mjs} +8 -4
- package/dist/nl-Y7LECDDR.mjs.map +1 -0
- package/dist/{no-XRA2UCQD.mjs → no-KEKCEWU6.mjs} +8 -4
- package/dist/no-KEKCEWU6.mjs.map +1 -0
- package/dist/{pl-WH6LJA5G.mjs → pl-7A7OC75O.mjs} +8 -4
- package/dist/pl-7A7OC75O.mjs.map +1 -0
- package/dist/{pt-7GAG57BM.mjs → pt-35HTM7RA.mjs} +8 -4
- package/dist/pt-35HTM7RA.mjs.map +1 -0
- package/dist/{ro-BTDDRB7N.mjs → ro-VAWL5KQA.mjs} +8 -4
- package/dist/ro-VAWL5KQA.mjs.map +1 -0
- package/dist/{sv-7V5C2IT4.mjs → sv-7ZK5EQEB.mjs} +8 -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-LPKYLBX5.mjs → th-UDWZ4X34.mjs} +8 -4
- package/dist/th-UDWZ4X34.mjs.map +1 -0
- package/dist/{tr-DU4RQL4M.mjs → tr-4WMPK3UX.mjs} +8 -4
- package/dist/tr-4WMPK3UX.mjs.map +1 -0
- package/dist/{uk-36UHTDDI.mjs → uk-SSLASQYJ.mjs} +8 -4
- package/dist/uk-SSLASQYJ.mjs.map +1 -0
- package/dist/{vi-GDHOUZDH.mjs → vi-IF42Z5PU.mjs} +8 -4
- package/dist/vi-IF42Z5PU.mjs.map +1 -0
- package/dist/{zh-TYUID4XZ.mjs → zh-HRQTNTAI.mjs} +8 -4
- package/dist/zh-HRQTNTAI.mjs.map +1 -0
- package/package.json +8 -2
- 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 -138
- package/src/components/resource/AnnotationHistory.tsx +12 -13
- package/src/components/resource/BrowseView.tsx +89 -177
- package/src/components/resource/HistoryEvent.tsx +16 -11
- package/src/components/resource/ResourceViewer.tsx +201 -370
- 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/event-formatting.ts +316 -0
- package/src/components/resource/panels/AssessmentEntry.tsx +25 -33
- package/src/components/resource/panels/AssessmentPanel.tsx +137 -31
- package/src/components/resource/panels/CollaborationPanel.tsx +20 -13
- package/src/components/resource/panels/CommentEntry.tsx +38 -32
- package/src/components/resource/panels/CommentsPanel.tsx +153 -31
- 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 +166 -49
- package/src/components/resource/panels/ResourceInfoPanel.tsx +47 -48
- package/src/components/resource/panels/StatisticsPanel.tsx +9 -19
- package/src/components/resource/panels/TagEntry.tsx +25 -33
- package/src/components/resource/panels/TaggingPanel.tsx +141 -25
- package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +46 -101
- package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +566 -0
- package/src/components/resource/panels/__tests__/CommentEntry.test.tsx +86 -78
- package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +146 -141
- 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 +228 -103
- package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +117 -61
- package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +586 -0
- 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 +16 -27
- 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 +145 -91
- package/src/features/resource-viewer/__tests__/detection-progress-flow.test.tsx +322 -0
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +325 -476
- package/src/styles/motivations/motivation-assessment.css +28 -0
- package/src/styles/patterns/panel-helpers.css +26 -0
- package/translations/ar.json +7 -3
- package/translations/bn.json +7 -3
- package/translations/cs.json +7 -3
- package/translations/da.json +7 -3
- package/translations/de.json +7 -3
- package/translations/el.json +7 -3
- package/translations/en.json +7 -3
- package/translations/es.json +7 -3
- package/translations/fa.json +7 -3
- package/translations/fi.json +7 -3
- package/translations/fr.json +7 -3
- package/translations/he.json +7 -3
- package/translations/hi.json +7 -3
- package/translations/id.json +7 -3
- package/translations/it.json +7 -3
- package/translations/ja.json +7 -3
- package/translations/ko.json +7 -3
- package/translations/ms.json +7 -3
- package/translations/nl.json +7 -3
- package/translations/no.json +7 -3
- package/translations/pl.json +7 -3
- package/translations/pt.json +7 -3
- package/translations/ro.json +7 -3
- package/translations/sv.json +7 -3
- package/translations/th.json +7 -3
- package/translations/tr.json +7 -3
- package/translations/uk.json +7 -3
- package/translations/vi.json +7 -3
- package/translations/zh.json +7 -3
- package/dist/PdfAnnotationCanvas.client-ADC4FFSE.mjs.map +0 -1
- package/dist/TranslationManager-Co_5fSxl.d.mts +0 -118
- package/dist/ar-RNNSPLQB.mjs.map +0 -1
- package/dist/bn-S2CDL7EC.mjs.map +0 -1
- package/dist/chunk-35LLVRFK.mjs.map +0 -1
- package/dist/chunk-UDX2Q35T.mjs.map +0 -1
- package/dist/cs-RSV675WU.mjs.map +0 -1
- package/dist/da-CHXNPWJC.mjs.map +0 -1
- package/dist/de-KPEZ53D4.mjs.map +0 -1
- package/dist/el-MW2BME5T.mjs.map +0 -1
- package/dist/es-HQ24NYS3.mjs.map +0 -1
- package/dist/fa-W34LRLHG.mjs.map +0 -1
- package/dist/fi-3U44IGOA.mjs.map +0 -1
- package/dist/fr-N7DKX6NN.mjs.map +0 -1
- package/dist/he-CS4WRXN3.mjs.map +0 -1
- package/dist/hi-GJDY46KA.mjs.map +0 -1
- package/dist/id-WAEZJK2Y.mjs.map +0 -1
- package/dist/it-VDNDMZPU.mjs.map +0 -1
- package/dist/ja-5PEH56J5.mjs.map +0 -1
- package/dist/ko-JYPL3WVA.mjs.map +0 -1
- package/dist/ms-5PZVW76T.mjs.map +0 -1
- package/dist/nl-YXES36KM.mjs.map +0 -1
- package/dist/no-XRA2UCQD.mjs.map +0 -1
- package/dist/pl-WH6LJA5G.mjs.map +0 -1
- package/dist/pt-7GAG57BM.mjs.map +0 -1
- package/dist/ro-BTDDRB7N.mjs.map +0 -1
- package/dist/sv-7V5C2IT4.mjs.map +0 -1
- package/dist/th-LPKYLBX5.mjs.map +0 -1
- package/dist/tr-DU4RQL4M.mjs.map +0 -1
- package/dist/uk-36UHTDDI.mjs.map +0 -1
- package/dist/vi-GDHOUZDH.mjs.map +0 -1
- package/dist/zh-TYUID4XZ.mjs.map +0 -1
- /package/dist/{en-EVMIX24Y.mjs.map → en-KJCJQ4OO.mjs.map} +0 -0
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
* All dependencies passed as props - no Next.js hooks!
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import React, { useState,
|
|
8
|
+
import React, { useState, useCallback, useRef } from 'react';
|
|
9
9
|
import type { components } from '@semiont/api-client';
|
|
10
10
|
import { getResourceId } from '@semiont/api-client';
|
|
11
|
-
import { useRovingTabIndex
|
|
11
|
+
import { useRovingTabIndex } from '../../../hooks/useRovingTabIndex';
|
|
12
|
+
import { Toolbar } from '../../../components/Toolbar';
|
|
12
13
|
import { ResourceCard } from './ResourceCard';
|
|
13
14
|
|
|
14
15
|
type ResourceDescriptor = components['schemas']['ResourceDescriptor'];
|
|
@@ -23,11 +24,8 @@ export interface ResourceDiscoveryPageProps {
|
|
|
23
24
|
|
|
24
25
|
// UI state props
|
|
25
26
|
theme: 'light' | 'dark';
|
|
26
|
-
onThemeChange: (theme: 'light' | 'dark') => void;
|
|
27
27
|
showLineNumbers: boolean;
|
|
28
|
-
onLineNumbersToggle: () => void;
|
|
29
28
|
activePanel: string | null;
|
|
30
|
-
onPanelToggle: (panel: string) => void;
|
|
31
29
|
|
|
32
30
|
// Navigation props
|
|
33
31
|
onNavigateToResource: (resourceId: string) => void;
|
|
@@ -64,11 +62,8 @@ export function ResourceDiscoveryPage({
|
|
|
64
62
|
isLoadingRecent,
|
|
65
63
|
isSearching,
|
|
66
64
|
theme,
|
|
67
|
-
onThemeChange,
|
|
68
65
|
showLineNumbers,
|
|
69
|
-
onLineNumbersToggle,
|
|
70
66
|
activePanel,
|
|
71
|
-
onPanelToggle,
|
|
72
67
|
onNavigateToResource,
|
|
73
68
|
onNavigateToCompose,
|
|
74
69
|
translations: t,
|
|
@@ -81,20 +76,13 @@ export function ResourceDiscoveryPage({
|
|
|
81
76
|
const hasSearchQuery = searchQuery.trim() !== '';
|
|
82
77
|
const hasSearchResults = searchDocuments.length > 0;
|
|
83
78
|
|
|
84
|
-
//
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (!selectedEntityType) return baseDocuments;
|
|
93
|
-
|
|
94
|
-
return baseDocuments.filter((resource: ResourceDescriptor) =>
|
|
95
|
-
resource.entityTypes && resource.entityTypes.includes(selectedEntityType)
|
|
96
|
-
);
|
|
97
|
-
}, [recentDocuments, searchDocuments, selectedEntityType, hasSearchResults]);
|
|
79
|
+
// Filtered documents
|
|
80
|
+
const baseDocuments = hasSearchResults ? searchDocuments : recentDocuments;
|
|
81
|
+
const filteredResources = !selectedEntityType
|
|
82
|
+
? baseDocuments
|
|
83
|
+
: baseDocuments.filter((resource: ResourceDescriptor) =>
|
|
84
|
+
resource.entityTypes && resource.entityTypes.includes(selectedEntityType)
|
|
85
|
+
);
|
|
98
86
|
|
|
99
87
|
// Roving tabindex for entity type filters
|
|
100
88
|
const entityFilterRoving = useRovingTabIndex<HTMLDivElement>(
|
|
@@ -108,6 +96,10 @@ export function ResourceDiscoveryPage({
|
|
|
108
96
|
{ orientation: 'grid', cols: 2 } // 2 columns on medium+ screens
|
|
109
97
|
);
|
|
110
98
|
|
|
99
|
+
// Store navigation callback in ref to avoid re-creating openResource
|
|
100
|
+
const onNavigateToResourceRef = useRef(onNavigateToResource);
|
|
101
|
+
onNavigateToResourceRef.current = onNavigateToResource;
|
|
102
|
+
|
|
111
103
|
// Memoized callbacks
|
|
112
104
|
const handleEntityTypeFilter = useCallback((entityType: string) => {
|
|
113
105
|
setSelectedEntityType(entityType);
|
|
@@ -116,9 +108,9 @@ export function ResourceDiscoveryPage({
|
|
|
116
108
|
const openResource = useCallback((resource: ResourceDescriptor) => {
|
|
117
109
|
const resourceId = getResourceId(resource);
|
|
118
110
|
if (resourceId) {
|
|
119
|
-
|
|
111
|
+
onNavigateToResourceRef.current(resourceId);
|
|
120
112
|
}
|
|
121
|
-
}, [
|
|
113
|
+
}, []);
|
|
122
114
|
|
|
123
115
|
const handleSearchSubmit = useCallback((e: React.FormEvent) => {
|
|
124
116
|
e.preventDefault();
|
|
@@ -274,16 +266,13 @@ export function ResourceDiscoveryPage({
|
|
|
274
266
|
<ToolbarPanels
|
|
275
267
|
activePanel={activePanel}
|
|
276
268
|
theme={theme}
|
|
277
|
-
onThemeChange={onThemeChange}
|
|
278
269
|
showLineNumbers={showLineNumbers}
|
|
279
|
-
onLineNumbersToggle={onLineNumbersToggle}
|
|
280
270
|
/>
|
|
281
271
|
|
|
282
272
|
{/* Toolbar - Always visible on the right */}
|
|
283
273
|
<Toolbar
|
|
284
274
|
context="simple"
|
|
285
275
|
activePanel={activePanel}
|
|
286
|
-
onPanelToggle={onPanelToggle}
|
|
287
276
|
/>
|
|
288
277
|
</div>
|
|
289
278
|
</div>
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer 3: Feature Integration Test - Annotation Deletion Flow Architecture
|
|
3
|
+
*
|
|
4
|
+
* Tests the COMPLETE annotation deletion flow with real component composition:
|
|
5
|
+
* - EventBusProvider (REAL)
|
|
6
|
+
* - ApiClientProvider (REAL, with MOCKED client)
|
|
7
|
+
* - useAnnotationFlow (REAL)
|
|
8
|
+
* - useEventOperations (REAL)
|
|
9
|
+
* - useEventSubscriptions (REAL)
|
|
10
|
+
*
|
|
11
|
+
* This test focuses on ARCHITECTURE and EVENT WIRING:
|
|
12
|
+
* - Verifies deletion API called exactly ONCE (catches duplicate subscriptions)
|
|
13
|
+
* - Tests event propagation through the event bus
|
|
14
|
+
* - Validates success/failure event emissions
|
|
15
|
+
* - Ensures auth token is passed correctly
|
|
16
|
+
*
|
|
17
|
+
* CRITICAL: This test prevents regressions where:
|
|
18
|
+
* - Multiple deletion paths exist (event-driven vs direct)
|
|
19
|
+
* - useEventOperations not called in useAnnotationFlow
|
|
20
|
+
* - Auth token missing from API calls (401 errors)
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
24
|
+
import { render, waitFor } from '@testing-library/react';
|
|
25
|
+
import { act } from 'react';
|
|
26
|
+
import { useAnnotationFlow } from '../../../hooks/useAnnotationFlow';
|
|
27
|
+
import { EventBusProvider, useEventBus, resetEventBusForTesting } from '../../../contexts/EventBusContext';
|
|
28
|
+
import { ApiClientProvider } from '../../../contexts/ApiClientContext';
|
|
29
|
+
import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
|
|
30
|
+
import { SemiontApiClient, resourceUri, accessToken } from '@semiont/api-client';
|
|
31
|
+
|
|
32
|
+
describe('Annotation Deletion - Feature Integration', () => {
|
|
33
|
+
let deleteAnnotationSpy: ReturnType<typeof vi.fn>;
|
|
34
|
+
const testUri = resourceUri('http://localhost:4000/resources/test-resource');
|
|
35
|
+
const testToken = 'test-token-123';
|
|
36
|
+
const testBaseUrl = 'http://localhost:4000';
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
vi.clearAllMocks();
|
|
40
|
+
resetEventBusForTesting();
|
|
41
|
+
|
|
42
|
+
// Mock the deleteAnnotation method on SemiontApiClient prototype
|
|
43
|
+
deleteAnnotationSpy = vi.fn().mockResolvedValue({ success: true });
|
|
44
|
+
vi.spyOn(SemiontApiClient.prototype, 'deleteAnnotation').mockImplementation(deleteAnnotationSpy);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
vi.restoreAllMocks();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Helper to render the annotation flow with real providers
|
|
53
|
+
*/
|
|
54
|
+
function renderAnnotationFlow() {
|
|
55
|
+
let eventBusInstance: ReturnType<typeof useEventBus> | null = null;
|
|
56
|
+
|
|
57
|
+
function TestComponent() {
|
|
58
|
+
eventBusInstance = useEventBus();
|
|
59
|
+
useAnnotationFlow(testUri);
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
render(
|
|
64
|
+
<AuthTokenProvider token={testToken}>
|
|
65
|
+
<EventBusProvider>
|
|
66
|
+
<ApiClientProvider baseUrl={testBaseUrl}>
|
|
67
|
+
<TestComponent />
|
|
68
|
+
</ApiClientProvider>
|
|
69
|
+
</EventBusProvider>
|
|
70
|
+
</AuthTokenProvider>
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
emitDelete: (annotationId: string) => {
|
|
75
|
+
act(() => {
|
|
76
|
+
eventBusInstance!.emit('annotation:delete', { annotationId });
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
eventBus: eventBusInstance!,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
it('should call deleteAnnotation API exactly ONCE when annotation:delete event is emitted', async () => {
|
|
84
|
+
const { emitDelete } = renderAnnotationFlow();
|
|
85
|
+
const annotationId = 'annotation-123';
|
|
86
|
+
|
|
87
|
+
// Trigger deletion via event bus (how UI triggers it)
|
|
88
|
+
emitDelete(annotationId);
|
|
89
|
+
|
|
90
|
+
// CRITICAL ASSERTION: API called exactly once (not twice!)
|
|
91
|
+
// This would FAIL if there were competing deletion paths
|
|
92
|
+
await waitFor(() => {
|
|
93
|
+
expect(deleteAnnotationSpy).toHaveBeenCalledTimes(1);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Verify correct parameters (annotationUri constructed from ID)
|
|
97
|
+
expect(deleteAnnotationSpy).toHaveBeenCalledWith(
|
|
98
|
+
expect.stringContaining(annotationId),
|
|
99
|
+
expect.objectContaining({
|
|
100
|
+
auth: accessToken(testToken),
|
|
101
|
+
})
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should pass auth token to API call (prevents 401 errors)', async () => {
|
|
106
|
+
const { emitDelete } = renderAnnotationFlow();
|
|
107
|
+
|
|
108
|
+
emitDelete('annotation-456');
|
|
109
|
+
|
|
110
|
+
await waitFor(() => {
|
|
111
|
+
expect(deleteAnnotationSpy).toHaveBeenCalled();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// CRITICAL: Auth token must be present
|
|
115
|
+
const callArgs = deleteAnnotationSpy.mock.calls[0];
|
|
116
|
+
expect(callArgs[1]).toHaveProperty('auth');
|
|
117
|
+
expect(callArgs[1].auth).toBe(accessToken(testToken));
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should emit annotation:deleted event on successful deletion', async () => {
|
|
121
|
+
const { emitDelete, eventBus } = renderAnnotationFlow();
|
|
122
|
+
const deletedListener = vi.fn();
|
|
123
|
+
|
|
124
|
+
// Subscribe to success event
|
|
125
|
+
eventBus.on('annotation:deleted', deletedListener);
|
|
126
|
+
|
|
127
|
+
emitDelete('annotation-789');
|
|
128
|
+
|
|
129
|
+
// Wait for API call to complete
|
|
130
|
+
await waitFor(() => {
|
|
131
|
+
expect(deleteAnnotationSpy).toHaveBeenCalled();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Verify success event was emitted
|
|
135
|
+
await waitFor(() => {
|
|
136
|
+
expect(deletedListener).toHaveBeenCalledWith({
|
|
137
|
+
annotationId: 'annotation-789',
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should emit annotation:delete-failed event on API error', async () => {
|
|
143
|
+
// Make API call fail
|
|
144
|
+
deleteAnnotationSpy.mockRejectedValue(new Error('Network error'));
|
|
145
|
+
|
|
146
|
+
const { emitDelete, eventBus } = renderAnnotationFlow();
|
|
147
|
+
const failedListener = vi.fn();
|
|
148
|
+
|
|
149
|
+
// Subscribe to failure event
|
|
150
|
+
eventBus.on('annotation:delete-failed', failedListener);
|
|
151
|
+
|
|
152
|
+
emitDelete('annotation-error');
|
|
153
|
+
|
|
154
|
+
// Wait for API call to be attempted
|
|
155
|
+
await waitFor(() => {
|
|
156
|
+
expect(deleteAnnotationSpy).toHaveBeenCalled();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Verify failure event was emitted
|
|
160
|
+
await waitFor(() => {
|
|
161
|
+
expect(failedListener).toHaveBeenCalledWith({
|
|
162
|
+
error: expect.any(Error),
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should handle multiple deletions in sequence without duplicate API calls', async () => {
|
|
168
|
+
const { emitDelete } = renderAnnotationFlow();
|
|
169
|
+
|
|
170
|
+
// Delete first annotation
|
|
171
|
+
emitDelete('annotation-1');
|
|
172
|
+
|
|
173
|
+
await waitFor(() => {
|
|
174
|
+
expect(deleteAnnotationSpy).toHaveBeenCalledTimes(1);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// Delete second annotation
|
|
178
|
+
emitDelete('annotation-2');
|
|
179
|
+
|
|
180
|
+
await waitFor(() => {
|
|
181
|
+
expect(deleteAnnotationSpy).toHaveBeenCalledTimes(2);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Verify each call had correct annotation ID
|
|
185
|
+
expect(deleteAnnotationSpy.mock.calls[0][0]).toContain('annotation-1');
|
|
186
|
+
expect(deleteAnnotationSpy.mock.calls[1][0]).toContain('annotation-2');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('ARCHITECTURE: useEventOperations is called in useAnnotationFlow (not elsewhere)', async () => {
|
|
190
|
+
/**
|
|
191
|
+
* This test validates that there's only ONE event-driven deletion path:
|
|
192
|
+
* - useAnnotationFlow calls useEventOperations
|
|
193
|
+
* - useEventOperations subscribes to annotation:delete
|
|
194
|
+
* - No other component/hook subscribes to annotation:delete
|
|
195
|
+
*
|
|
196
|
+
* If this test fails, it means either:
|
|
197
|
+
* 1. useEventOperations removed from useAnnotationFlow (CRITICAL BUG)
|
|
198
|
+
* 2. Duplicate subscription added elsewhere (ARCHITECTURE VIOLATION)
|
|
199
|
+
*/
|
|
200
|
+
|
|
201
|
+
const { emitDelete } = renderAnnotationFlow();
|
|
202
|
+
|
|
203
|
+
emitDelete('architecture-test');
|
|
204
|
+
|
|
205
|
+
// Single API call = single subscription = correct architecture
|
|
206
|
+
await waitFor(() => {
|
|
207
|
+
expect(deleteAnnotationSpy).toHaveBeenCalledTimes(1);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('REGRESSION: No direct deleteAnnotation function in ResourceAnnotationsContext', () => {
|
|
212
|
+
/**
|
|
213
|
+
* This test prevents regression to the old pattern where
|
|
214
|
+
* ResourceAnnotationsContext had a deleteAnnotation function
|
|
215
|
+
* that bypassed the event bus.
|
|
216
|
+
*
|
|
217
|
+
* The correct pattern is event-driven only:
|
|
218
|
+
* - UI emits annotation:delete event
|
|
219
|
+
* - useEventOperations handles it
|
|
220
|
+
* - No direct function calls
|
|
221
|
+
*/
|
|
222
|
+
|
|
223
|
+
// This would fail to compile if deleteAnnotation was added back to context
|
|
224
|
+
// Type-level enforcement via TypeScript
|
|
225
|
+
const { emitDelete } = renderAnnotationFlow();
|
|
226
|
+
emitDelete('regression-test');
|
|
227
|
+
|
|
228
|
+
// Deletion still works via events
|
|
229
|
+
expect(deleteAnnotationSpy).toHaveBeenCalled();
|
|
230
|
+
});
|
|
231
|
+
});
|
|
@@ -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
|
+
});
|