@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.
Files changed (213) 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-EMHEHPCJ.mjs → ar-4ZEORRW2.mjs} +7 -4
  5. package/dist/ar-4ZEORRW2.mjs.map +1 -0
  6. package/dist/{bn-OVCI4F6X.mjs → bn-SEDE5BQJ.mjs} +7 -4
  7. package/dist/bn-SEDE5BQJ.mjs.map +1 -0
  8. package/dist/{chunk-LIHZTECW.mjs → chunk-D7NBW4RV.mjs} +7 -4
  9. package/dist/chunk-D7NBW4RV.mjs.map +1 -0
  10. package/dist/{chunk-JZIO2A3B.mjs → chunk-ZR4ZV2LY.mjs} +206 -146
  11. package/dist/chunk-ZR4ZV2LY.mjs.map +1 -0
  12. package/dist/{cs-FAN66Q2F.mjs → cs-7W4WF5WD.mjs} +7 -4
  13. package/dist/cs-7W4WF5WD.mjs.map +1 -0
  14. package/dist/{da-YBBIHI2O.mjs → da-75XGBCBK.mjs} +7 -4
  15. package/dist/da-75XGBCBK.mjs.map +1 -0
  16. package/dist/{de-MAYU33LB.mjs → de-ODJVFLHM.mjs} +7 -4
  17. package/dist/de-ODJVFLHM.mjs.map +1 -0
  18. package/dist/{el-MKGSWN4O.mjs → el-C4PM4WB3.mjs} +7 -4
  19. package/dist/el-C4PM4WB3.mjs.map +1 -0
  20. package/dist/{en-DDLIXJCU.mjs → en-KJCJQ4OO.mjs} +2 -2
  21. package/dist/{es-52LHUWJD.mjs → es-WD33R7QL.mjs} +7 -4
  22. package/dist/es-WD33R7QL.mjs.map +1 -0
  23. package/dist/{fa-FJICRANB.mjs → fa-2BP6V56P.mjs} +7 -4
  24. package/dist/fa-2BP6V56P.mjs.map +1 -0
  25. package/dist/{fi-O455XFCR.mjs → fi-USRRW24J.mjs} +7 -4
  26. package/dist/fi-USRRW24J.mjs.map +1 -0
  27. package/dist/{fr-TXIXHOOE.mjs → fr-EC5S6WVF.mjs} +7 -4
  28. package/dist/fr-EC5S6WVF.mjs.map +1 -0
  29. package/dist/{he-JBSOX5IN.mjs → he-7TBVIKAA.mjs} +7 -4
  30. package/dist/he-7TBVIKAA.mjs.map +1 -0
  31. package/dist/{hi-KGHI3XVT.mjs → hi-FO4VIZLA.mjs} +7 -4
  32. package/dist/hi-FO4VIZLA.mjs.map +1 -0
  33. package/dist/{id-5OCPPZLO.mjs → id-7U7GGVWY.mjs} +7 -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 +645 -471
  38. package/dist/index.mjs +3461 -3025
  39. package/dist/index.mjs.map +1 -1
  40. package/dist/{it-PNBBZSM2.mjs → it-Y4OPL6I2.mjs} +7 -4
  41. package/dist/it-Y4OPL6I2.mjs.map +1 -0
  42. package/dist/{ja-LDD7R3TJ.mjs → ja-PK7SQL55.mjs} +7 -4
  43. package/dist/ja-PK7SQL55.mjs.map +1 -0
  44. package/dist/{ko-F47ZDEY3.mjs → ko-L25PXMYD.mjs} +7 -4
  45. package/dist/ko-L25PXMYD.mjs.map +1 -0
  46. package/dist/{ms-Z7LMXJWL.mjs → ms-STH777QM.mjs} +7 -4
  47. package/dist/ms-STH777QM.mjs.map +1 -0
  48. package/dist/{nl-6SJFBPJ3.mjs → nl-Y7LECDDR.mjs} +7 -4
  49. package/dist/nl-Y7LECDDR.mjs.map +1 -0
  50. package/dist/{no-YXPBPSGF.mjs → no-KEKCEWU6.mjs} +7 -4
  51. package/dist/no-KEKCEWU6.mjs.map +1 -0
  52. package/dist/{pl-P4AZ2QME.mjs → pl-7A7OC75O.mjs} +7 -4
  53. package/dist/pl-7A7OC75O.mjs.map +1 -0
  54. package/dist/{pt-LHWUS6U6.mjs → pt-35HTM7RA.mjs} +7 -4
  55. package/dist/pt-35HTM7RA.mjs.map +1 -0
  56. package/dist/{ro-EA5J2ZON.mjs → ro-VAWL5KQA.mjs} +7 -4
  57. package/dist/ro-VAWL5KQA.mjs.map +1 -0
  58. package/dist/{sv-DATBS3UQ.mjs → sv-7ZK5EQEB.mjs} +7 -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-WTFJRWPT.mjs → th-UDWZ4X34.mjs} +7 -4
  64. package/dist/th-UDWZ4X34.mjs.map +1 -0
  65. package/dist/{tr-IKO3RXOX.mjs → tr-4WMPK3UX.mjs} +7 -4
  66. package/dist/tr-4WMPK3UX.mjs.map +1 -0
  67. package/dist/{uk-CF6CTTRK.mjs → uk-SSLASQYJ.mjs} +7 -4
  68. package/dist/uk-SSLASQYJ.mjs.map +1 -0
  69. package/dist/{vi-AJLTXPZQ.mjs → vi-IF42Z5PU.mjs} +7 -4
  70. package/dist/vi-IF42Z5PU.mjs.map +1 -0
  71. package/dist/{zh-U3ORHHYH.mjs → zh-HRQTNTAI.mjs} +7 -4
  72. package/dist/zh-HRQTNTAI.mjs.map +1 -0
  73. package/package.json +3 -1
  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 -134
  100. package/src/components/resource/BrowseView.tsx +86 -166
  101. package/src/components/resource/HistoryEvent.tsx +13 -7
  102. package/src/components/resource/ResourceViewer.tsx +122 -264
  103. package/src/components/resource/__tests__/BrowseView.test.tsx +631 -0
  104. package/src/components/resource/__tests__/ResourceViewer.mode-switch.test.tsx +231 -0
  105. package/src/components/resource/panels/AssessmentEntry.tsx +25 -33
  106. package/src/components/resource/panels/AssessmentPanel.tsx +106 -28
  107. package/src/components/resource/panels/CommentEntry.tsx +38 -32
  108. package/src/components/resource/panels/CommentsPanel.tsx +121 -28
  109. package/src/components/resource/panels/DetectSection.css +36 -1
  110. package/src/components/resource/panels/DetectSection.tsx +38 -10
  111. package/src/components/resource/panels/HighlightEntry.tsx +25 -33
  112. package/src/components/resource/panels/HighlightPanel.tsx +100 -25
  113. package/src/components/resource/panels/ReferenceEntry.tsx +61 -75
  114. package/src/components/resource/panels/ReferencesPanel.tsx +134 -42
  115. package/src/components/resource/panels/ResourceInfoPanel.tsx +47 -48
  116. package/src/components/resource/panels/TagEntry.tsx +25 -33
  117. package/src/components/resource/panels/TaggingPanel.tsx +119 -30
  118. package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +30 -92
  119. package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +129 -110
  120. package/src/components/resource/panels/__tests__/CommentEntry.test.tsx +86 -78
  121. package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +144 -149
  122. package/src/components/resource/panels/__tests__/DetectSection.test.tsx +480 -0
  123. package/src/components/resource/panels/__tests__/HighlightPanel.detectionProgress.test.tsx +362 -0
  124. package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +226 -111
  125. package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +117 -61
  126. package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +128 -106
  127. package/src/components/settings/SettingsPanel.tsx +15 -12
  128. package/src/features/admin-devops/__tests__/AdminDevOpsPage.test.tsx +1 -46
  129. package/src/features/admin-devops/components/AdminDevOpsPage.tsx +0 -9
  130. package/src/features/admin-security/__tests__/AdminSecurityPage.test.tsx +0 -3
  131. package/src/features/admin-security/components/AdminSecurityPage.tsx +0 -9
  132. package/src/features/admin-users/__tests__/AdminUsersPage.test.tsx +0 -3
  133. package/src/features/admin-users/components/AdminUsersPage.tsx +0 -9
  134. package/src/features/moderate-entity-tags/__tests__/EntityTagsPage.test.tsx +0 -3
  135. package/src/features/moderate-entity-tags/components/EntityTagsPage.tsx +1 -9
  136. package/src/features/moderate-recent/__tests__/RecentDocumentsPage.test.tsx +0 -32
  137. package/src/features/moderate-recent/components/RecentDocumentsPage.tsx +1 -9
  138. package/src/features/moderate-tag-schemas/__tests__/TagSchemasPage.test.tsx +0 -32
  139. package/src/features/moderate-tag-schemas/components/TagSchemasPage.tsx +1 -9
  140. package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +51 -54
  141. package/src/features/resource-compose/components/ResourceComposePage.tsx +3 -13
  142. package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +39 -45
  143. package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +9 -13
  144. package/src/features/resource-viewer/__tests__/AnnotationDeletionIntegration.test.tsx +231 -0
  145. package/src/features/resource-viewer/__tests__/DetectionFlowBug.test.tsx +234 -0
  146. package/src/features/resource-viewer/__tests__/DetectionFlowIntegration.test.tsx +388 -0
  147. package/src/features/resource-viewer/__tests__/DetectionProgressDismissal.test.tsx +318 -0
  148. package/src/features/resource-viewer/__tests__/GenerationFlowIntegration.test.tsx +504 -0
  149. package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +135 -88
  150. package/src/features/resource-viewer/__tests__/detection-progress-flow.test.tsx +322 -0
  151. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +308 -528
  152. package/translations/ar.json +6 -3
  153. package/translations/bn.json +6 -3
  154. package/translations/cs.json +6 -3
  155. package/translations/da.json +6 -3
  156. package/translations/de.json +6 -3
  157. package/translations/el.json +6 -3
  158. package/translations/en.json +6 -3
  159. package/translations/es.json +6 -3
  160. package/translations/fa.json +6 -3
  161. package/translations/fi.json +6 -3
  162. package/translations/fr.json +6 -3
  163. package/translations/he.json +6 -3
  164. package/translations/hi.json +6 -3
  165. package/translations/id.json +6 -3
  166. package/translations/it.json +6 -3
  167. package/translations/ja.json +6 -3
  168. package/translations/ko.json +6 -3
  169. package/translations/ms.json +6 -3
  170. package/translations/nl.json +6 -3
  171. package/translations/no.json +6 -3
  172. package/translations/pl.json +6 -3
  173. package/translations/pt.json +6 -3
  174. package/translations/ro.json +6 -3
  175. package/translations/sv.json +6 -3
  176. package/translations/th.json +6 -3
  177. package/translations/tr.json +6 -3
  178. package/translations/uk.json +6 -3
  179. package/translations/vi.json +6 -3
  180. package/translations/zh.json +6 -3
  181. package/dist/PdfAnnotationCanvas.client-ADC4FFSE.mjs.map +0 -1
  182. package/dist/TranslationManager-Co_5fSxl.d.mts +0 -118
  183. package/dist/ar-EMHEHPCJ.mjs.map +0 -1
  184. package/dist/bn-OVCI4F6X.mjs.map +0 -1
  185. package/dist/chunk-JZIO2A3B.mjs.map +0 -1
  186. package/dist/chunk-LIHZTECW.mjs.map +0 -1
  187. package/dist/cs-FAN66Q2F.mjs.map +0 -1
  188. package/dist/da-YBBIHI2O.mjs.map +0 -1
  189. package/dist/de-MAYU33LB.mjs.map +0 -1
  190. package/dist/el-MKGSWN4O.mjs.map +0 -1
  191. package/dist/es-52LHUWJD.mjs.map +0 -1
  192. package/dist/fa-FJICRANB.mjs.map +0 -1
  193. package/dist/fi-O455XFCR.mjs.map +0 -1
  194. package/dist/fr-TXIXHOOE.mjs.map +0 -1
  195. package/dist/he-JBSOX5IN.mjs.map +0 -1
  196. package/dist/hi-KGHI3XVT.mjs.map +0 -1
  197. package/dist/id-5OCPPZLO.mjs.map +0 -1
  198. package/dist/it-PNBBZSM2.mjs.map +0 -1
  199. package/dist/ja-LDD7R3TJ.mjs.map +0 -1
  200. package/dist/ko-F47ZDEY3.mjs.map +0 -1
  201. package/dist/ms-Z7LMXJWL.mjs.map +0 -1
  202. package/dist/nl-6SJFBPJ3.mjs.map +0 -1
  203. package/dist/no-YXPBPSGF.mjs.map +0 -1
  204. package/dist/pl-P4AZ2QME.mjs.map +0 -1
  205. package/dist/pt-LHWUS6U6.mjs.map +0 -1
  206. package/dist/ro-EA5J2ZON.mjs.map +0 -1
  207. package/dist/sv-DATBS3UQ.mjs.map +0 -1
  208. package/dist/th-WTFJRWPT.mjs.map +0 -1
  209. package/dist/tr-IKO3RXOX.mjs.map +0 -1
  210. package/dist/uk-CF6CTTRK.mjs.map +0 -1
  211. package/dist/vi-AJLTXPZQ.mjs.map +0 -1
  212. package/dist/zh-U3ORHHYH.mjs.map +0 -1
  213. /package/dist/{en-DDLIXJCU.mjs.map → en-KJCJQ4OO.mjs.map} +0 -0
@@ -2,13 +2,53 @@
2
2
  * Tests for ResourceViewerPage component
3
3
  *
4
4
  * Tests the main resource viewer UI component.
5
- * No Next.js mocking required - all dependencies passed as props!
5
+ * All internal data fetching (content, annotations, etc.) is mocked at the hook level.
6
6
  */
7
7
 
8
- import { describe, it, expect, vi } from 'vitest';
8
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
9
9
  import { render, screen } from '@testing-library/react';
10
+ import React from 'react';
10
11
  import { ResourceViewerPage } from '../components/ResourceViewerPage';
11
12
  import type { ResourceViewerPageProps } from '../components/ResourceViewerPage';
13
+ // Import directly from context file to bypass mocked barrel export
14
+ import { EventBusProvider, resetEventBusForTesting } from '../../../contexts/EventBusContext';
15
+ import { ApiClientProvider } from '../../../contexts/ApiClientContext';
16
+ import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
17
+ import { ToastProvider } from '../../../components/Toast';
18
+
19
+ // jsdom doesn't implement window.matchMedia — mock it for useTheme
20
+ Object.defineProperty(window, 'matchMedia', {
21
+ writable: true,
22
+ value: vi.fn().mockImplementation((query: string) => ({
23
+ matches: false,
24
+ media: query,
25
+ onchange: null,
26
+ addListener: vi.fn(),
27
+ removeListener: vi.fn(),
28
+ addEventListener: vi.fn(),
29
+ removeEventListener: vi.fn(),
30
+ dispatchEvent: vi.fn(),
31
+ })),
32
+ });
33
+
34
+ // Mock internal hooks that fetch data
35
+ vi.mock('../../../hooks/useResourceContent', () => ({
36
+ useResourceContent: () => ({ content: 'Test content', loading: false }),
37
+ }));
38
+
39
+ vi.mock('../../../lib/api-hooks', () => ({
40
+ useResources: () => ({
41
+ annotations: { useQuery: () => ({ data: { annotations: [] } }) },
42
+ referencedBy: { useQuery: () => ({ data: { referencedBy: [] }, isLoading: false }) },
43
+ }),
44
+ useEntityTypes: () => ({
45
+ list: { useQuery: () => ({ data: { entityTypes: ['Document', 'Article', 'Book'] } }) },
46
+ }),
47
+ }));
48
+
49
+ vi.mock('../../../hooks/useResourceEvents', () => ({
50
+ useResourceEvents: () => null,
51
+ }));
12
52
 
13
53
  // Mock dependencies that ResourceViewerPage imports
14
54
  vi.mock('@tanstack/react-query', async () => {
@@ -24,7 +64,6 @@ vi.mock('@tanstack/react-query', async () => {
24
64
 
25
65
  vi.mock('@semiont/react-ui', async () => {
26
66
  const actual = await vi.importActual('@semiont/react-ui');
27
- const mitt = await import('mitt');
28
67
  return {
29
68
  ...actual,
30
69
  ResourceViewer: ({ resource }: any) => <div data-testid="resource-viewer">{resource.name}</div>,
@@ -39,16 +78,47 @@ vi.mock('@semiont/react-ui', async () => {
39
78
  createCancelDetectionHandler: () => vi.fn(),
40
79
  useGenerationProgress: () => ({
41
80
  progress: null,
42
- startGeneration: vi.fn(),
43
81
  clearProgress: vi.fn(),
44
82
  }),
45
83
  useDebouncedCallback: (fn: any) => fn,
46
84
  supportsDetection: () => false,
47
85
  MakeMeaningEventBusProvider: ({ children }: any) => children,
48
- useMakeMeaningEvents: () => mitt.default(),
86
+ useResourceLoadingAnnouncements: () => ({
87
+ announceResourceLoading: vi.fn(),
88
+ announceResourceLoaded: vi.fn(),
89
+ }),
90
+ // Don't mock EventBusProvider, useEventBus, resetEventBusForTesting - let actual pass through via ...actual
91
+ useEventSubscriptions: vi.fn(),
92
+ useResourceAnnotations: () => ({
93
+ clearNewAnnotationId: vi.fn(),
94
+ newAnnotationIds: new Set(),
95
+ createAnnotation: vi.fn(),
96
+ deleteAnnotation: vi.fn(),
97
+ triggerSparkleAnimation: vi.fn(),
98
+ }),
49
99
  };
50
100
  });
51
101
 
102
+ vi.mock('../../../contexts/OpenResourcesContext', () => ({
103
+ useOpenResources: () => ({
104
+ openResources: [],
105
+ addResource: vi.fn(),
106
+ removeResource: vi.fn(),
107
+ isResourceOpen: vi.fn().mockReturnValue(false),
108
+ }),
109
+ }));
110
+
111
+ vi.mock('../../../contexts/ResourceAnnotationsContext', () => ({
112
+ useResourceAnnotations: () => ({
113
+ clearNewAnnotationId: vi.fn(),
114
+ newAnnotationIds: new Set(),
115
+ createAnnotation: vi.fn(),
116
+ deleteAnnotation: vi.fn(),
117
+ triggerSparkleAnimation: vi.fn(),
118
+ }),
119
+ ResourceAnnotationsProvider: ({ children }: any) => children,
120
+ }));
121
+
52
122
  vi.mock('@/components/toolbar/ToolbarPanels', () => ({
53
123
  ToolbarPanels: ({ children }: any) => <div data-testid="toolbar-panels">{children}</div>,
54
124
  }));
@@ -61,7 +131,7 @@ vi.mock('@/components/modals/GenerationConfigModal', () => ({
61
131
  GenerationConfigModal: () => <div data-testid="generation-modal">Generation Modal</div>,
62
132
  }));
63
133
 
64
- // Create mock props with all required fields
134
+ // Create mock props matching the current ResourceViewerPageProps
65
135
  const createMockProps = (overrides?: Partial<ResourceViewerPageProps>): ResourceViewerPageProps => ({
66
136
  resource: {
67
137
  '@context': 'https://www.w3.org/ns/anno.jsonld',
@@ -80,45 +150,40 @@ const createMockProps = (overrides?: Partial<ResourceViewerPageProps>): Resource
80
150
  ],
81
151
  },
82
152
  rUri: 'http://localhost/resources/test-123' as any,
83
- content: 'Test content for the resource viewer',
84
- contentLoading: false,
85
- annotations: [],
86
- referencedBy: [],
87
- referencedByLoading: false,
88
- allEntityTypes: ['Document', 'Article', 'Book'],
89
153
  locale: 'en',
90
- theme: 'light',
91
- onThemeChange: vi.fn(),
92
- showLineNumbers: false,
93
- onLineNumbersToggle: vi.fn(),
94
- activePanel: null,
95
- onPanelToggle: vi.fn(),
96
- setActivePanel: vi.fn(),
97
- onArchive: vi.fn(),
98
- onUnarchive: vi.fn(),
99
- onClone: vi.fn(),
100
- onUpdateAnnotationBody: vi.fn(),
101
- onRefetchAnnotations: vi.fn(),
102
- onCreateAnnotation: vi.fn(),
103
- onTriggerSparkleAnimation: vi.fn(),
104
- onClearNewAnnotationId: vi.fn(),
105
- showSuccess: vi.fn(),
106
- showError: vi.fn(),
107
154
  cacheManager: {},
108
- client: {},
109
155
  Link: ({ children }: any) => <a>{children}</a>,
110
156
  routes: {},
111
- ToolbarPanels: ({ children }: any) => <div data-testid="toolbar-panels">{children}</div>,
157
+ refetchDocument: vi.fn().mockResolvedValue(undefined),
158
+ ToolbarPanels: ({ children, activePanel }: any) =>
159
+ !activePanel ? null : <div data-testid="toolbar-panels">{children}</div>,
112
160
  SearchResourcesModal: () => <div data-testid="search-modal">Search Modal</div>,
113
161
  GenerationConfigModal: () => <div data-testid="generation-modal">Generation Modal</div>,
114
162
  ...overrides,
115
163
  });
116
164
 
165
+ // Test wrapper to provide all required providers
166
+ const renderWithProviders = (ui: React.ReactElement) => {
167
+ return render(
168
+ <ToastProvider>
169
+ <AuthTokenProvider token={null}>
170
+ <ApiClientProvider baseUrl="http://localhost:4000">
171
+ <EventBusProvider>{ui}</EventBusProvider>
172
+ </ApiClientProvider>
173
+ </AuthTokenProvider>
174
+ </ToastProvider>
175
+ );
176
+ };
177
+
117
178
  describe('ResourceViewerPage', () => {
179
+ beforeEach(() => {
180
+ resetEventBusForTesting();
181
+ });
182
+
118
183
  describe('Basic Rendering', () => {
119
184
  it('renders without crashing', () => {
120
185
  const props = createMockProps();
121
- render(<ResourceViewerPage {...props} />);
186
+ renderWithProviders(<ResourceViewerPage {...props} />);
122
187
 
123
188
  // Check for header element specifically
124
189
  expect(screen.getByRole('heading', { name: 'Test Resource' })).toBeInTheDocument();
@@ -132,30 +197,23 @@ describe('ResourceViewerPage', () => {
132
197
  },
133
198
  });
134
199
 
135
- render(<ResourceViewerPage {...props} />);
200
+ renderWithProviders(<ResourceViewerPage {...props} />);
136
201
 
137
202
  expect(screen.getByRole('heading', { name: 'My Special Resource' })).toBeInTheDocument();
138
203
  });
139
204
 
140
205
  it('renders toolbar component', () => {
141
206
  const props = createMockProps();
142
- render(<ResourceViewerPage {...props} />);
207
+ renderWithProviders(<ResourceViewerPage {...props} />);
143
208
 
144
209
  expect(screen.getByTestId('toolbar')).toBeInTheDocument();
145
210
  });
146
211
  });
147
212
 
148
213
  describe('Content Loading', () => {
149
- it('shows loading message when content is loading', () => {
150
- const props = createMockProps({ contentLoading: true });
151
- render(<ResourceViewerPage {...props} />);
152
-
153
- expect(screen.getByText('Loading document content...')).toBeInTheDocument();
154
- });
155
-
156
214
  it('shows ResourceViewer when content is loaded', () => {
157
- const props = createMockProps({ contentLoading: false });
158
- render(<ResourceViewerPage {...props} />);
215
+ const props = createMockProps();
216
+ renderWithProviders(<ResourceViewerPage {...props} />);
159
217
 
160
218
  expect(screen.getByTestId('resource-viewer')).toBeInTheDocument();
161
219
  });
@@ -163,43 +221,53 @@ describe('ResourceViewerPage', () => {
163
221
 
164
222
  describe('Panel Visibility', () => {
165
223
  it('shows annotations panel when activePanel is annotations', () => {
166
- const props = createMockProps({ activePanel: 'annotations' });
167
- render(<ResourceViewerPage {...props} />);
224
+ localStorage.setItem('activeToolbarPanel', 'annotations');
225
+ const props = createMockProps();
226
+ renderWithProviders(<ResourceViewerPage {...props} />);
168
227
 
169
228
  expect(screen.getByTestId('annotations-panel')).toBeInTheDocument();
229
+ localStorage.clear();
170
230
  });
171
231
 
172
232
  it('shows history panel when activePanel is history', () => {
173
- const props = createMockProps({ activePanel: 'history' });
174
- render(<ResourceViewerPage {...props} />);
233
+ localStorage.setItem('activeToolbarPanel', 'history');
234
+ const props = createMockProps();
235
+ renderWithProviders(<ResourceViewerPage {...props} />);
175
236
 
176
237
  expect(screen.getByTestId('history-panel')).toBeInTheDocument();
238
+ localStorage.clear();
177
239
  });
178
240
 
179
241
  it('shows info panel when activePanel is info', () => {
180
- const props = createMockProps({ activePanel: 'info' });
181
- render(<ResourceViewerPage {...props} />);
242
+ localStorage.setItem('activeToolbarPanel', 'info');
243
+ const props = createMockProps();
244
+ renderWithProviders(<ResourceViewerPage {...props} />);
182
245
 
183
246
  expect(screen.getByTestId('info-panel')).toBeInTheDocument();
247
+ localStorage.clear();
184
248
  });
185
249
 
186
250
  it('shows collaboration panel when activePanel is collaboration', () => {
187
- const props = createMockProps({ activePanel: 'collaboration' });
188
- render(<ResourceViewerPage {...props} />);
251
+ localStorage.setItem('activeToolbarPanel', 'collaboration');
252
+ const props = createMockProps();
253
+ renderWithProviders(<ResourceViewerPage {...props} />);
189
254
 
190
255
  expect(screen.getByTestId('collaboration-panel')).toBeInTheDocument();
256
+ localStorage.clear();
191
257
  });
192
258
 
193
259
  it('shows jsonld panel when activePanel is jsonld', () => {
194
- const props = createMockProps({ activePanel: 'jsonld' });
195
- render(<ResourceViewerPage {...props} />);
260
+ localStorage.setItem('activeToolbarPanel', 'jsonld');
261
+ const props = createMockProps();
262
+ renderWithProviders(<ResourceViewerPage {...props} />);
196
263
 
197
264
  expect(screen.getByTestId('jsonld-panel')).toBeInTheDocument();
265
+ localStorage.clear();
198
266
  });
199
267
  });
200
268
 
201
269
  describe('Archived Status', () => {
202
- it('shows archived badge when resource is archived and in annotate mode', () => {
270
+ it('does not show archived badge when not in annotate mode', () => {
203
271
  const props = createMockProps({
204
272
  resource: {
205
273
  ...createMockProps().resource,
@@ -207,10 +275,9 @@ describe('ResourceViewerPage', () => {
207
275
  },
208
276
  });
209
277
 
210
- render(<ResourceViewerPage {...props} />);
278
+ renderWithProviders(<ResourceViewerPage {...props} />);
211
279
 
212
280
  // Archived badge only shows in annotate mode, which defaults to false
213
- // So we test that it doesn't show when not in annotate mode
214
281
  expect(screen.queryByText('📦 Archived')).not.toBeInTheDocument();
215
282
  });
216
283
  });
@@ -218,58 +285,38 @@ describe('ResourceViewerPage', () => {
218
285
  describe('Modals', () => {
219
286
  it('renders search resources modal', () => {
220
287
  const props = createMockProps();
221
- render(<ResourceViewerPage {...props} />);
288
+ renderWithProviders(<ResourceViewerPage {...props} />);
222
289
 
223
290
  expect(screen.getByTestId('search-modal')).toBeInTheDocument();
224
291
  });
225
292
 
226
293
  it('renders generation config modal', () => {
227
294
  const props = createMockProps();
228
- render(<ResourceViewerPage {...props} />);
295
+ renderWithProviders(<ResourceViewerPage {...props} />);
229
296
 
230
297
  expect(screen.getByTestId('generation-modal')).toBeInTheDocument();
231
298
  });
232
299
  });
233
300
 
234
301
  describe('Props Integration', () => {
235
- it('passes resource content to ResourceViewer', () => {
236
- const props = createMockProps({
237
- content: 'Custom test content',
238
- contentLoading: false,
239
- });
240
-
241
- render(<ResourceViewerPage {...props} />);
302
+ it('renders ResourceViewer component', () => {
303
+ const props = createMockProps();
304
+ renderWithProviders(<ResourceViewerPage {...props} />);
242
305
 
243
- // ResourceViewer is mocked to show resource name
244
306
  expect(screen.getByTestId('resource-viewer')).toBeInTheDocument();
245
307
  });
246
308
 
247
- it('handles multiple annotations', () => {
309
+ it('renders with different resource names', () => {
248
310
  const props = createMockProps({
249
- annotations: [
250
- {
251
- '@context': 'http://www.w3.org/ns/anno.jsonld',
252
- id: 'http://localhost/annotations/1',
253
- type: 'Annotation',
254
- motivation: 'commenting',
255
- body: [],
256
- target: 'http://localhost/resources/test-123',
257
- },
258
- {
259
- '@context': 'http://www.w3.org/ns/anno.jsonld',
260
- id: 'http://localhost/annotations/2',
261
- type: 'Annotation',
262
- motivation: 'highlighting',
263
- body: [],
264
- target: 'http://localhost/resources/test-123',
265
- },
266
- ],
311
+ resource: {
312
+ ...createMockProps().resource,
313
+ name: 'Different Resource Name',
314
+ },
267
315
  });
268
316
 
269
- render(<ResourceViewerPage {...props} />);
317
+ renderWithProviders(<ResourceViewerPage {...props} />);
270
318
 
271
- // Component should render without errors - check for header
272
- expect(screen.getByRole('heading', { name: 'Test Resource' })).toBeInTheDocument();
319
+ expect(screen.getByRole('heading', { name: 'Different Resource Name' })).toBeInTheDocument();
273
320
  });
274
321
  });
275
322
  });
@@ -0,0 +1,322 @@
1
+ /**
2
+ * Layer 3 Integration Test: Detection Progress Flow UI/UX
3
+ *
4
+ * Tests the complete data flow from UI → EventBus → useEventOperations → SSE (mocked)
5
+ *
6
+ * This test uses COMPOSITION instead of mocking:
7
+ * - Real React components composed together (useDetectionFlow + HighlightPanel + DetectSection)
8
+ * - Real EventBus (mitt) passed via context
9
+ * - Real useEventOperations hook with mock API client passed as prop
10
+ * - Mock SSE stream (simulated API responses) provided via composition
11
+ *
12
+ * This test focuses on USER EXPERIENCE:
13
+ * - Verifies user clicks "Detect" button and sees progress
14
+ * - Tests progress messages appear and update correctly
15
+ * - Validates final message stays visible after completion
16
+ * - Ensures progress clears on error
17
+ *
18
+ * COMPLEMENTARY TEST: See DetectionFlowIntegration.test.tsx for architecture testing
19
+ * - That test verifies SYSTEM ARCHITECTURE (event wiring, API call count)
20
+ * - This test verifies USER EXPERIENCE (button clicks, UI feedback)
21
+ *
22
+ * UPDATED: Now tests useDetectionFlow hook instead of DetectionFlowContainer
23
+ */
24
+
25
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
26
+ import { render, screen, waitFor } from '@testing-library/react';
27
+ import userEvent from '@testing-library/user-event';
28
+ import { act } from 'react';
29
+ import { HighlightPanel } from '../../../components/resource/panels/HighlightPanel';
30
+ import { useDetectionFlow } from '../../../hooks/useDetectionFlow';
31
+ import { EventBusProvider, resetEventBusForTesting } from '../../../contexts/EventBusContext';
32
+ import { ApiClientProvider } from '../../../contexts/ApiClientContext';
33
+ import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
34
+ import { SSEClient } from '@semiont/api-client';
35
+ import type { components } from '@semiont/api-client';
36
+
37
+ type Annotation = components['schemas']['Annotation'];
38
+
39
+ // Mock translations
40
+ const mockT = vi.fn((key: string) => {
41
+ const translations: Record<string, string> = {
42
+ title: 'Highlights',
43
+ noHighlights: 'No highlights yet',
44
+ detectHighlights: 'Detect Highlights',
45
+ instructions: 'Instructions',
46
+ optional: '(optional)',
47
+ instructionsPlaceholder: 'Enter custom instructions...',
48
+ densityLabel: 'Density',
49
+ densitySparse: 'Sparse',
50
+ densityDense: 'Dense',
51
+ detect: 'Detect',
52
+ };
53
+ return translations[key] || key;
54
+ });
55
+
56
+ vi.mock('../../../contexts/TranslationContext', () => ({
57
+ useTranslations: () => mockT,
58
+ TranslationProvider: ({ children }: { children: React.ReactNode }) => children,
59
+ }));
60
+
61
+ // Create a mock SSE stream that we can control
62
+ class MockSSEStream {
63
+ private progressHandlers: Array<(chunk: any) => void> = [];
64
+ private completeHandlers: Array<(finalChunk?: any) => void> = [];
65
+ private errorHandlers: Array<(error: Error) => void> = [];
66
+
67
+ onProgress(handler: (chunk: any) => void) {
68
+ this.progressHandlers.push(handler);
69
+ }
70
+
71
+ onComplete(handler: (finalChunk?: any) => void) {
72
+ this.completeHandlers.push(handler);
73
+ }
74
+
75
+ onError(handler: (error: Error) => void) {
76
+ this.errorHandlers.push(handler);
77
+ }
78
+
79
+ // Test helper methods
80
+ emitProgress(chunk: any) {
81
+ this.progressHandlers.forEach(handler => handler(chunk));
82
+ }
83
+
84
+ emitComplete(finalChunk?: any) {
85
+ this.completeHandlers.forEach(handler => handler(finalChunk));
86
+ }
87
+
88
+ emitError(error: Error) {
89
+ this.errorHandlers.forEach(handler => handler(error));
90
+ }
91
+ }
92
+
93
+ // Composition: Test component that wires together the pieces we're testing
94
+ function DetectionFlowTestHarness({
95
+ rUri,
96
+ annotations,
97
+ }: {
98
+ rUri: string;
99
+ annotations: Annotation[];
100
+ }) {
101
+ const { detectingMotivation, detectionProgress } = useDetectionFlow(rUri as any);
102
+
103
+ return (
104
+ <HighlightPanel
105
+ annotations={annotations}
106
+ pendingAnnotation={null}
107
+ hoveredAnnotationId={null}
108
+ isDetecting={detectingMotivation === 'highlighting'}
109
+ detectionProgress={detectionProgress}
110
+ annotateMode={true}
111
+ />
112
+ );
113
+ }
114
+
115
+ describe('Detection Progress Flow Integration (Layer 3)', () => {
116
+ let mockAnnotations: Annotation[];
117
+ let mockStream: MockSSEStream;
118
+ const rUri = 'https://example.com/resources/test-resource-1';
119
+
120
+ // Helper to render test harness with composition
121
+ const renderDetectionFlow = () => {
122
+ return render(
123
+ <EventBusProvider>
124
+ <AuthTokenProvider token={null}>
125
+ <ApiClientProvider baseUrl="http://localhost:4000">
126
+ <DetectionFlowTestHarness
127
+ rUri={rUri}
128
+ annotations={mockAnnotations}
129
+ />
130
+ </ApiClientProvider>
131
+ </AuthTokenProvider>
132
+ </EventBusProvider>
133
+ );
134
+ };
135
+
136
+ beforeEach(() => {
137
+ // Reset event bus for test isolation
138
+ resetEventBusForTesting();
139
+ vi.clearAllMocks();
140
+
141
+ // Reset mocks
142
+ mockStream = new MockSSEStream();
143
+
144
+ // Spy on SSEClient prototype methods to inject mock stream
145
+ vi.spyOn(SSEClient.prototype, 'detectHighlights').mockReturnValue(mockStream as any);
146
+ vi.spyOn(SSEClient.prototype, 'detectAssessments').mockReturnValue(mockStream as any);
147
+ vi.spyOn(SSEClient.prototype, 'detectComments').mockReturnValue(mockStream as any);
148
+ vi.spyOn(SSEClient.prototype, 'detectAnnotations').mockReturnValue(mockStream as any);
149
+
150
+ mockAnnotations = [];
151
+ });
152
+
153
+ afterEach(() => {
154
+ vi.restoreAllMocks();
155
+ });
156
+
157
+ it('should display detection progress from button click to completion', async () => {
158
+ const user = userEvent.setup();
159
+
160
+ // Render composed components with real EventBus and mock API client
161
+ renderDetectionFlow();
162
+
163
+ // Initial state: no progress visible
164
+ expect(screen.queryByText(/Analyzing/)).not.toBeInTheDocument();
165
+
166
+ // Click detect button
167
+ const detectButton = screen.getByRole('button', { name: /✨ Detect/ });
168
+ await user.click(detectButton);
169
+
170
+ // Simulate SSE progress chunk #1: Starting
171
+ act(() => {
172
+ mockStream.emitProgress({
173
+ status: 'started',
174
+ percentage: 0,
175
+ message: 'Starting detection...',
176
+ });
177
+ });
178
+
179
+ // Verify progress message appears
180
+ await waitFor(() => {
181
+ expect(screen.getByText('Starting detection...')).toBeInTheDocument();
182
+ });
183
+
184
+ // Simulate SSE progress chunk #2: Analyzing
185
+ act(() => {
186
+ mockStream.emitProgress({
187
+ status: 'analyzing',
188
+ percentage: 30,
189
+ message: 'Analyzing text...',
190
+ });
191
+ });
192
+
193
+ await waitFor(() => {
194
+ expect(screen.getByText('Analyzing text...')).toBeInTheDocument();
195
+ });
196
+
197
+ // Simulate SSE progress chunk #3: Creating annotations
198
+ act(() => {
199
+ mockStream.emitProgress({
200
+ status: 'creating',
201
+ percentage: 60,
202
+ message: 'Creating 14 annotations...',
203
+ });
204
+ });
205
+
206
+ await waitFor(() => {
207
+ expect(screen.getByText('Creating 14 annotations...')).toBeInTheDocument();
208
+ });
209
+
210
+ // Simulate stream completion with final chunk (onComplete receives the final progress)
211
+ act(() => {
212
+ mockStream.emitComplete({
213
+ status: 'complete',
214
+ percentage: 100,
215
+ message: 'Complete! Created 14 highlights',
216
+ });
217
+ });
218
+
219
+ // CRITICAL TEST: Final message should still be visible after completion
220
+ await waitFor(() => {
221
+ expect(screen.getByText('Complete! Created 14 highlights')).toBeInTheDocument();
222
+ });
223
+ });
224
+
225
+ it('should handle out-of-order SSE events (complete before final progress)', async () => {
226
+ const user = userEvent.setup();
227
+
228
+ renderDetectionFlow();
229
+
230
+ // Click detect
231
+ const detectButton = screen.getByRole('button', { name: /✨ Detect/ });
232
+ await user.click(detectButton);
233
+
234
+ // Simulate initial progress
235
+ act(() => {
236
+ mockStream.emitProgress({
237
+ status: 'analyzing',
238
+ message: 'Analyzing...',
239
+ });
240
+ });
241
+
242
+ await waitFor(() => {
243
+ expect(screen.getByText('Analyzing...')).toBeInTheDocument();
244
+ });
245
+
246
+ // Simulate race condition: complete arrives BEFORE final progress
247
+ act(() => {
248
+ mockStream.emitComplete();
249
+ });
250
+
251
+ // Then final progress chunk arrives
252
+ act(() => {
253
+ mockStream.emitProgress({
254
+ status: 'complete',
255
+ percentage: 100,
256
+ message: 'Complete! Created 5 highlights',
257
+ });
258
+ });
259
+
260
+ // Final message should still be visible
261
+ await waitFor(() => {
262
+ expect(screen.getByText('Complete! Created 5 highlights')).toBeInTheDocument();
263
+ });
264
+ });
265
+
266
+ it('should show progress with request parameters', async () => {
267
+ const user = userEvent.setup();
268
+
269
+ renderDetectionFlow();
270
+
271
+ const detectButton = screen.getByRole('button', { name: /✨ Detect/ });
272
+ await user.click(detectButton);
273
+
274
+ // Simulate progress with request parameters
275
+ act(() => {
276
+ mockStream.emitProgress({
277
+ status: 'analyzing',
278
+ message: 'Analyzing with custom instructions...',
279
+ requestParams: [
280
+ { label: 'Instructions', value: 'Find important points' },
281
+ { label: 'Density', value: '5' },
282
+ ],
283
+ });
284
+ });
285
+
286
+ await waitFor(() => {
287
+ expect(screen.getByText('Find important points')).toBeInTheDocument();
288
+ expect(screen.getByText('5')).toBeInTheDocument();
289
+ });
290
+ });
291
+
292
+ it('should clear progress on detection:failed', async () => {
293
+ const user = userEvent.setup();
294
+
295
+ renderDetectionFlow();
296
+
297
+ const detectButton = screen.getByRole('button', { name: /✨ Detect/ });
298
+ await user.click(detectButton);
299
+
300
+ // Show progress
301
+ act(() => {
302
+ mockStream.emitProgress({
303
+ status: 'analyzing',
304
+ message: 'Analyzing...',
305
+ });
306
+ });
307
+
308
+ await waitFor(() => {
309
+ expect(screen.getByText('Analyzing...')).toBeInTheDocument();
310
+ });
311
+
312
+ // Simulate error
313
+ act(() => {
314
+ mockStream.emitError(new Error('Network timeout'));
315
+ });
316
+
317
+ // Progress should be cleared
318
+ await waitFor(() => {
319
+ expect(screen.queryByText('Analyzing...')).not.toBeInTheDocument();
320
+ });
321
+ });
322
+ });