@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.
Files changed (219) hide show
  1. package/dist/EventBusContext-7GvDyO0d.d.mts +414 -0
  2. package/dist/{PdfAnnotationCanvas.client-ADC4FFSE.mjs → PdfAnnotationCanvas.client-RAJRPQLU.mjs} +42 -27
  3. package/dist/PdfAnnotationCanvas.client-RAJRPQLU.mjs.map +1 -0
  4. package/dist/{ar-RNNSPLQB.mjs → ar-4ZEORRW2.mjs} +8 -4
  5. package/dist/ar-4ZEORRW2.mjs.map +1 -0
  6. package/dist/{bn-S2CDL7EC.mjs → bn-SEDE5BQJ.mjs} +8 -4
  7. package/dist/bn-SEDE5BQJ.mjs.map +1 -0
  8. package/dist/{chunk-UDX2Q35T.mjs → chunk-D7NBW4RV.mjs} +8 -4
  9. package/dist/chunk-D7NBW4RV.mjs.map +1 -0
  10. package/dist/{chunk-35LLVRFK.mjs → chunk-ZR4ZV2LY.mjs} +206 -146
  11. package/dist/chunk-ZR4ZV2LY.mjs.map +1 -0
  12. package/dist/{cs-RSV675WU.mjs → cs-7W4WF5WD.mjs} +8 -4
  13. package/dist/cs-7W4WF5WD.mjs.map +1 -0
  14. package/dist/{da-CHXNPWJC.mjs → da-75XGBCBK.mjs} +8 -4
  15. package/dist/da-75XGBCBK.mjs.map +1 -0
  16. package/dist/{de-KPEZ53D4.mjs → de-ODJVFLHM.mjs} +8 -4
  17. package/dist/de-ODJVFLHM.mjs.map +1 -0
  18. package/dist/{el-MW2BME5T.mjs → el-C4PM4WB3.mjs} +8 -4
  19. package/dist/el-C4PM4WB3.mjs.map +1 -0
  20. package/dist/{en-EVMIX24Y.mjs → en-KJCJQ4OO.mjs} +2 -2
  21. package/dist/{es-HQ24NYS3.mjs → es-WD33R7QL.mjs} +8 -4
  22. package/dist/es-WD33R7QL.mjs.map +1 -0
  23. package/dist/{fa-W34LRLHG.mjs → fa-2BP6V56P.mjs} +8 -4
  24. package/dist/fa-2BP6V56P.mjs.map +1 -0
  25. package/dist/{fi-3U44IGOA.mjs → fi-USRRW24J.mjs} +8 -4
  26. package/dist/fi-USRRW24J.mjs.map +1 -0
  27. package/dist/{fr-N7DKX6NN.mjs → fr-EC5S6WVF.mjs} +8 -4
  28. package/dist/fr-EC5S6WVF.mjs.map +1 -0
  29. package/dist/{he-CS4WRXN3.mjs → he-7TBVIKAA.mjs} +8 -4
  30. package/dist/he-7TBVIKAA.mjs.map +1 -0
  31. package/dist/{hi-GJDY46KA.mjs → hi-FO4VIZLA.mjs} +8 -4
  32. package/dist/hi-FO4VIZLA.mjs.map +1 -0
  33. package/dist/{id-WAEZJK2Y.mjs → id-7U7GGVWY.mjs} +8 -4
  34. package/dist/id-7U7GGVWY.mjs.map +1 -0
  35. package/dist/index.css +123 -85
  36. package/dist/index.css.map +1 -1
  37. package/dist/index.d.mts +699 -529
  38. package/dist/index.mjs +4291 -3491
  39. package/dist/index.mjs.map +1 -1
  40. package/dist/{it-VDNDMZPU.mjs → it-Y4OPL6I2.mjs} +8 -4
  41. package/dist/it-Y4OPL6I2.mjs.map +1 -0
  42. package/dist/{ja-5PEH56J5.mjs → ja-PK7SQL55.mjs} +8 -4
  43. package/dist/ja-PK7SQL55.mjs.map +1 -0
  44. package/dist/{ko-JYPL3WVA.mjs → ko-L25PXMYD.mjs} +8 -4
  45. package/dist/ko-L25PXMYD.mjs.map +1 -0
  46. package/dist/{ms-5PZVW76T.mjs → ms-STH777QM.mjs} +8 -4
  47. package/dist/ms-STH777QM.mjs.map +1 -0
  48. package/dist/{nl-YXES36KM.mjs → nl-Y7LECDDR.mjs} +8 -4
  49. package/dist/nl-Y7LECDDR.mjs.map +1 -0
  50. package/dist/{no-XRA2UCQD.mjs → no-KEKCEWU6.mjs} +8 -4
  51. package/dist/no-KEKCEWU6.mjs.map +1 -0
  52. package/dist/{pl-WH6LJA5G.mjs → pl-7A7OC75O.mjs} +8 -4
  53. package/dist/pl-7A7OC75O.mjs.map +1 -0
  54. package/dist/{pt-7GAG57BM.mjs → pt-35HTM7RA.mjs} +8 -4
  55. package/dist/pt-35HTM7RA.mjs.map +1 -0
  56. package/dist/{ro-BTDDRB7N.mjs → ro-VAWL5KQA.mjs} +8 -4
  57. package/dist/ro-VAWL5KQA.mjs.map +1 -0
  58. package/dist/{sv-7V5C2IT4.mjs → sv-7ZK5EQEB.mjs} +8 -4
  59. package/dist/sv-7ZK5EQEB.mjs.map +1 -0
  60. package/dist/test-utils.d.mts +18 -8
  61. package/dist/test-utils.mjs +36 -14
  62. package/dist/test-utils.mjs.map +1 -1
  63. package/dist/{th-LPKYLBX5.mjs → th-UDWZ4X34.mjs} +8 -4
  64. package/dist/th-UDWZ4X34.mjs.map +1 -0
  65. package/dist/{tr-DU4RQL4M.mjs → tr-4WMPK3UX.mjs} +8 -4
  66. package/dist/tr-4WMPK3UX.mjs.map +1 -0
  67. package/dist/{uk-36UHTDDI.mjs → uk-SSLASQYJ.mjs} +8 -4
  68. package/dist/uk-SSLASQYJ.mjs.map +1 -0
  69. package/dist/{vi-GDHOUZDH.mjs → vi-IF42Z5PU.mjs} +8 -4
  70. package/dist/vi-IF42Z5PU.mjs.map +1 -0
  71. package/dist/{zh-TYUID4XZ.mjs → zh-HRQTNTAI.mjs} +8 -4
  72. package/dist/zh-HRQTNTAI.mjs.map +1 -0
  73. package/package.json +8 -2
  74. package/src/components/CodeMirrorRenderer.tsx +66 -93
  75. package/src/components/DetectionProgressWidget.tsx +16 -5
  76. package/src/components/LiveRegion.tsx +18 -18
  77. package/src/components/ResizeHandle.tsx +10 -4
  78. package/src/components/SessionTimer.tsx +2 -2
  79. package/src/components/Toolbar.tsx +18 -9
  80. package/src/components/__tests__/SessionTimer.test.tsx +9 -9
  81. package/src/components/annotation/AnnotateToolbar.tsx +17 -15
  82. package/src/components/annotation/__tests__/AnnotateToolbar.test.tsx +165 -63
  83. package/src/components/annotation/annotation-entries.css +10 -0
  84. package/src/components/annotation-popups/JsonLdView.tsx +8 -2
  85. package/src/components/image-annotation/AnnotationOverlay.tsx +42 -22
  86. package/src/components/image-annotation/SvgDrawingCanvas.tsx +27 -30
  87. package/src/components/layout/__tests__/LeftSidebar.test.tsx +12 -33
  88. package/src/components/layout/__tests__/PageLayout.test.tsx +37 -32
  89. package/src/components/layout/__tests__/UnifiedHeader.test.tsx +21 -40
  90. package/src/components/modals/ResourceSearchModal.tsx +2 -2
  91. package/src/components/modals/SearchModal.tsx +1 -1
  92. package/src/components/navigation/CollapsibleResourceNavigation.tsx +14 -9
  93. package/src/components/navigation/NavigationTabs.css +36 -24
  94. package/src/components/navigation/ObservableLink.tsx +91 -0
  95. package/src/components/navigation/SimpleNavigation.tsx +20 -16
  96. package/src/components/navigation/SortableResourceTab.tsx +11 -5
  97. package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +51 -26
  98. package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +28 -22
  99. package/src/components/resource/AnnotateView.tsx +64 -138
  100. package/src/components/resource/AnnotationHistory.tsx +12 -13
  101. package/src/components/resource/BrowseView.tsx +89 -177
  102. package/src/components/resource/HistoryEvent.tsx +16 -11
  103. package/src/components/resource/ResourceViewer.tsx +201 -370
  104. package/src/components/resource/__tests__/BrowseView.test.tsx +631 -0
  105. package/src/components/resource/__tests__/ResourceViewer.mode-switch.test.tsx +231 -0
  106. package/src/components/resource/event-formatting.ts +316 -0
  107. package/src/components/resource/panels/AssessmentEntry.tsx +25 -33
  108. package/src/components/resource/panels/AssessmentPanel.tsx +137 -31
  109. package/src/components/resource/panels/CollaborationPanel.tsx +20 -13
  110. package/src/components/resource/panels/CommentEntry.tsx +38 -32
  111. package/src/components/resource/panels/CommentsPanel.tsx +153 -31
  112. package/src/components/resource/panels/DetectSection.css +36 -1
  113. package/src/components/resource/panels/DetectSection.tsx +38 -10
  114. package/src/components/resource/panels/HighlightEntry.tsx +25 -33
  115. package/src/components/resource/panels/HighlightPanel.tsx +100 -25
  116. package/src/components/resource/panels/ReferenceEntry.tsx +61 -75
  117. package/src/components/resource/panels/ReferencesPanel.tsx +166 -49
  118. package/src/components/resource/panels/ResourceInfoPanel.tsx +47 -48
  119. package/src/components/resource/panels/StatisticsPanel.tsx +9 -19
  120. package/src/components/resource/panels/TagEntry.tsx +25 -33
  121. package/src/components/resource/panels/TaggingPanel.tsx +141 -25
  122. package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +46 -101
  123. package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +566 -0
  124. package/src/components/resource/panels/__tests__/CommentEntry.test.tsx +86 -78
  125. package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +146 -141
  126. package/src/components/resource/panels/__tests__/DetectSection.test.tsx +480 -0
  127. package/src/components/resource/panels/__tests__/HighlightPanel.detectionProgress.test.tsx +362 -0
  128. package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +228 -103
  129. package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +117 -61
  130. package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +586 -0
  131. package/src/components/settings/SettingsPanel.tsx +15 -12
  132. package/src/features/admin-devops/__tests__/AdminDevOpsPage.test.tsx +1 -46
  133. package/src/features/admin-devops/components/AdminDevOpsPage.tsx +0 -9
  134. package/src/features/admin-security/__tests__/AdminSecurityPage.test.tsx +0 -3
  135. package/src/features/admin-security/components/AdminSecurityPage.tsx +0 -9
  136. package/src/features/admin-users/__tests__/AdminUsersPage.test.tsx +0 -3
  137. package/src/features/admin-users/components/AdminUsersPage.tsx +0 -9
  138. package/src/features/moderate-entity-tags/__tests__/EntityTagsPage.test.tsx +0 -3
  139. package/src/features/moderate-entity-tags/components/EntityTagsPage.tsx +1 -9
  140. package/src/features/moderate-recent/__tests__/RecentDocumentsPage.test.tsx +0 -32
  141. package/src/features/moderate-recent/components/RecentDocumentsPage.tsx +1 -9
  142. package/src/features/moderate-tag-schemas/__tests__/TagSchemasPage.test.tsx +0 -32
  143. package/src/features/moderate-tag-schemas/components/TagSchemasPage.tsx +1 -9
  144. package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +51 -54
  145. package/src/features/resource-compose/components/ResourceComposePage.tsx +3 -13
  146. package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +39 -45
  147. package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +16 -27
  148. package/src/features/resource-viewer/__tests__/AnnotationDeletionIntegration.test.tsx +231 -0
  149. package/src/features/resource-viewer/__tests__/DetectionFlowBug.test.tsx +234 -0
  150. package/src/features/resource-viewer/__tests__/DetectionFlowIntegration.test.tsx +388 -0
  151. package/src/features/resource-viewer/__tests__/DetectionProgressDismissal.test.tsx +318 -0
  152. package/src/features/resource-viewer/__tests__/GenerationFlowIntegration.test.tsx +504 -0
  153. package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +145 -91
  154. package/src/features/resource-viewer/__tests__/detection-progress-flow.test.tsx +322 -0
  155. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +325 -476
  156. package/src/styles/motivations/motivation-assessment.css +28 -0
  157. package/src/styles/patterns/panel-helpers.css +26 -0
  158. package/translations/ar.json +7 -3
  159. package/translations/bn.json +7 -3
  160. package/translations/cs.json +7 -3
  161. package/translations/da.json +7 -3
  162. package/translations/de.json +7 -3
  163. package/translations/el.json +7 -3
  164. package/translations/en.json +7 -3
  165. package/translations/es.json +7 -3
  166. package/translations/fa.json +7 -3
  167. package/translations/fi.json +7 -3
  168. package/translations/fr.json +7 -3
  169. package/translations/he.json +7 -3
  170. package/translations/hi.json +7 -3
  171. package/translations/id.json +7 -3
  172. package/translations/it.json +7 -3
  173. package/translations/ja.json +7 -3
  174. package/translations/ko.json +7 -3
  175. package/translations/ms.json +7 -3
  176. package/translations/nl.json +7 -3
  177. package/translations/no.json +7 -3
  178. package/translations/pl.json +7 -3
  179. package/translations/pt.json +7 -3
  180. package/translations/ro.json +7 -3
  181. package/translations/sv.json +7 -3
  182. package/translations/th.json +7 -3
  183. package/translations/tr.json +7 -3
  184. package/translations/uk.json +7 -3
  185. package/translations/vi.json +7 -3
  186. package/translations/zh.json +7 -3
  187. package/dist/PdfAnnotationCanvas.client-ADC4FFSE.mjs.map +0 -1
  188. package/dist/TranslationManager-Co_5fSxl.d.mts +0 -118
  189. package/dist/ar-RNNSPLQB.mjs.map +0 -1
  190. package/dist/bn-S2CDL7EC.mjs.map +0 -1
  191. package/dist/chunk-35LLVRFK.mjs.map +0 -1
  192. package/dist/chunk-UDX2Q35T.mjs.map +0 -1
  193. package/dist/cs-RSV675WU.mjs.map +0 -1
  194. package/dist/da-CHXNPWJC.mjs.map +0 -1
  195. package/dist/de-KPEZ53D4.mjs.map +0 -1
  196. package/dist/el-MW2BME5T.mjs.map +0 -1
  197. package/dist/es-HQ24NYS3.mjs.map +0 -1
  198. package/dist/fa-W34LRLHG.mjs.map +0 -1
  199. package/dist/fi-3U44IGOA.mjs.map +0 -1
  200. package/dist/fr-N7DKX6NN.mjs.map +0 -1
  201. package/dist/he-CS4WRXN3.mjs.map +0 -1
  202. package/dist/hi-GJDY46KA.mjs.map +0 -1
  203. package/dist/id-WAEZJK2Y.mjs.map +0 -1
  204. package/dist/it-VDNDMZPU.mjs.map +0 -1
  205. package/dist/ja-5PEH56J5.mjs.map +0 -1
  206. package/dist/ko-JYPL3WVA.mjs.map +0 -1
  207. package/dist/ms-5PZVW76T.mjs.map +0 -1
  208. package/dist/nl-YXES36KM.mjs.map +0 -1
  209. package/dist/no-XRA2UCQD.mjs.map +0 -1
  210. package/dist/pl-WH6LJA5G.mjs.map +0 -1
  211. package/dist/pt-7GAG57BM.mjs.map +0 -1
  212. package/dist/ro-BTDDRB7N.mjs.map +0 -1
  213. package/dist/sv-7V5C2IT4.mjs.map +0 -1
  214. package/dist/th-LPKYLBX5.mjs.map +0 -1
  215. package/dist/tr-DU4RQL4M.mjs.map +0 -1
  216. package/dist/uk-36UHTDDI.mjs.map +0 -1
  217. package/dist/vi-GDHOUZDH.mjs.map +0 -1
  218. package/dist/zh-TYUID4XZ.mjs.map +0 -1
  219. /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, useMemo, useCallback } from 'react';
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, Toolbar } from '@semiont/react-ui';
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
- // Memoized filtered documents
85
- const filteredResources = useMemo(() => {
86
- // If we have search results, show them; otherwise show recent
87
- // This ensures we show recent docs even when search returns nothing
88
- const baseDocuments = hasSearchResults
89
- ? searchDocuments
90
- : recentDocuments;
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
- onNavigateToResource(resourceId);
111
+ onNavigateToResourceRef.current(resourceId);
120
112
  }
121
- }, [onNavigateToResource]);
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
+ });