@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
@@ -9,8 +9,9 @@
9
9
  import React, { useState, useEffect } from 'react';
10
10
  import type { components } from '@semiont/api-client';
11
11
  import { isImageMimeType, isPdfMimeType, LOCALES } from '@semiont/api-client';
12
- import { buttonStyles, CodeMirrorRenderer } from '@semiont/react-ui';
13
- import { useFormAnnouncements } from '@semiont/react-ui';
12
+ import { buttonStyles } from '../../../lib/button-styles';
13
+ import { CodeMirrorRenderer } from '../../../components/CodeMirrorRenderer';
14
+ import { useFormAnnouncements } from '../../../components/LiveRegion';
14
15
 
15
16
  type ResourceDescriptor = components['schemas']['ResourceDescriptor'];
16
17
 
@@ -40,11 +41,8 @@ export interface ResourceComposePageProps {
40
41
 
41
42
  // UI state
42
43
  theme: 'light' | 'dark';
43
- onThemeChange: (theme: 'light' | 'dark') => void;
44
44
  showLineNumbers: boolean;
45
- onLineNumbersToggle: () => void;
46
45
  activePanel: string | null;
47
- onPanelToggle: (panel: string) => void;
48
46
 
49
47
  // Actions
50
48
  onSaveResource: (params: SaveResourceParams) => Promise<void>;
@@ -111,11 +109,8 @@ export function ResourceComposePage({
111
109
  availableEntityTypes,
112
110
  initialLocale,
113
111
  theme,
114
- onThemeChange,
115
112
  showLineNumbers,
116
- onLineNumbersToggle,
117
113
  activePanel,
118
- onPanelToggle,
119
114
  onSaveResource,
120
115
  onCancel,
121
116
  translations: t,
@@ -529,12 +524,10 @@ export function ResourceComposePage({
529
524
  <div className="semiont-form__editor-wrapper" lang={selectedLanguage}>
530
525
  <CodeMirrorRenderer
531
526
  content={newResourceContent}
532
- segments={[]}
533
527
  editable={!isCreating}
534
528
  sourceView={true}
535
529
  showLineNumbers={showLineNumbers}
536
530
  onChange={(newContent) => setNewResourceContent(newContent)}
537
- annotators={{}}
538
531
  />
539
532
  </div>
540
533
  </div>
@@ -610,16 +603,13 @@ export function ResourceComposePage({
610
603
  <ToolbarPanels
611
604
  activePanel={activePanel}
612
605
  theme={theme}
613
- onThemeChange={onThemeChange}
614
606
  showLineNumbers={showLineNumbers}
615
- onLineNumbersToggle={onLineNumbersToggle}
616
607
  />
617
608
 
618
609
  {/* Toolbar - Always visible on the right */}
619
610
  <Toolbar
620
611
  context="simple"
621
612
  activePanel={activePanel}
622
- onPanelToggle={onPanelToggle}
623
613
  />
624
614
  </div>
625
615
  </div>
@@ -5,23 +5,11 @@
5
5
  * No Next.js mocking required - all dependencies passed as props!
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, fireEvent, waitFor } from '@testing-library/react';
10
10
  import { ResourceDiscoveryPage } from '../components/ResourceDiscoveryPage';
11
11
  import type { ResourceDiscoveryPageProps } from '../components/ResourceDiscoveryPage';
12
-
13
- // Mock dependencies
14
- vi.mock('@semiont/react-ui', async () => {
15
- const actual = await vi.importActual('@semiont/react-ui');
16
- return {
17
- ...actual,
18
- useRovingTabIndex: () => ({
19
- containerRef: { current: null },
20
- handleKeyDown: vi.fn(),
21
- }),
22
- Toolbar: () => <div data-testid="toolbar">Toolbar</div>,
23
- };
24
- });
12
+ import { EventBusProvider, resetEventBusForTesting } from '../../../contexts/EventBusContext';
25
13
 
26
14
  const createMockResource = (id: string, name: string, entityTypes: string[] = []) => ({
27
15
  '@context': 'https://www.w3.org/ns/anno.jsonld',
@@ -42,11 +30,8 @@ const createMockProps = (overrides?: Partial<ResourceDiscoveryPageProps>): Resou
42
30
  isLoadingRecent: false,
43
31
  isSearching: false,
44
32
  theme: 'light',
45
- onThemeChange: vi.fn(),
46
33
  showLineNumbers: false,
47
- onLineNumbersToggle: vi.fn(),
48
34
  activePanel: null,
49
- onPanelToggle: vi.fn(),
50
35
  onNavigateToResource: vi.fn(),
51
36
  onNavigateToCompose: vi.fn(),
52
37
  translations: {
@@ -71,18 +56,27 @@ const createMockProps = (overrides?: Partial<ResourceDiscoveryPageProps>): Resou
71
56
  ...overrides,
72
57
  });
73
58
 
59
+ // Helper to render with EventBusProvider
60
+ const renderWithProviders = (ui: React.ReactElement) => {
61
+ return render(<EventBusProvider>{ui}</EventBusProvider>);
62
+ };
63
+
74
64
  describe('ResourceDiscoveryPage', () => {
65
+ beforeEach(() => {
66
+ resetEventBusForTesting();
67
+ });
68
+
75
69
  describe('Basic Rendering', () => {
76
70
  it('renders without crashing', () => {
77
71
  const props = createMockProps();
78
- render(<ResourceDiscoveryPage {...props} />);
72
+ renderWithProviders(<ResourceDiscoveryPage {...props} />);
79
73
 
80
74
  expect(screen.getByText('Discover Resources')).toBeInTheDocument();
81
75
  });
82
76
 
83
77
  it('displays page title and subtitle', () => {
84
78
  const props = createMockProps();
85
- render(<ResourceDiscoveryPage {...props} />);
79
+ renderWithProviders(<ResourceDiscoveryPage {...props} />);
86
80
 
87
81
  expect(screen.getByText('Discover Resources')).toBeInTheDocument();
88
82
  expect(screen.getByText('Search and browse available resources')).toBeInTheDocument();
@@ -90,37 +84,39 @@ describe('ResourceDiscoveryPage', () => {
90
84
 
91
85
  it('renders search input', () => {
92
86
  const props = createMockProps();
93
- render(<ResourceDiscoveryPage {...props} />);
87
+ renderWithProviders(<ResourceDiscoveryPage {...props} />);
94
88
 
95
89
  expect(screen.getByPlaceholderText('Search resources...')).toBeInTheDocument();
96
90
  });
97
91
 
98
92
  it('renders search button', () => {
99
93
  const props = createMockProps();
100
- render(<ResourceDiscoveryPage {...props} />);
94
+ renderWithProviders(<ResourceDiscoveryPage {...props} />);
101
95
 
102
96
  expect(screen.getByRole('button', { name: 'Search' })).toBeInTheDocument();
103
97
  });
104
98
 
105
99
  it('renders toolbar component', () => {
106
100
  const props = createMockProps();
107
- render(<ResourceDiscoveryPage {...props} />);
101
+ const { container } = renderWithProviders(<ResourceDiscoveryPage {...props} />);
108
102
 
109
- expect(screen.getByTestId('toolbar')).toBeInTheDocument();
103
+ // Toolbar renders with context="simple" - check for toolbar element
104
+ const toolbar = container.querySelector('.semiont-toolbar[data-context="simple"]');
105
+ expect(toolbar).toBeInTheDocument();
110
106
  });
111
107
  });
112
108
 
113
109
  describe('Loading State', () => {
114
110
  it('shows loading message when isLoadingRecent is true', () => {
115
111
  const props = createMockProps({ isLoadingRecent: true });
116
- render(<ResourceDiscoveryPage {...props} />);
112
+ renderWithProviders(<ResourceDiscoveryPage {...props} />);
117
113
 
118
114
  expect(screen.getByText('Loading knowledge base...')).toBeInTheDocument();
119
115
  });
120
116
 
121
117
  it('does not show main content when loading', () => {
122
118
  const props = createMockProps({ isLoadingRecent: true });
123
- render(<ResourceDiscoveryPage {...props} />);
119
+ renderWithProviders(<ResourceDiscoveryPage {...props} />);
124
120
 
125
121
  expect(screen.queryByText('Discover Resources')).not.toBeInTheDocument();
126
122
  });
@@ -135,7 +131,7 @@ describe('ResourceDiscoveryPage', () => {
135
131
  ];
136
132
 
137
133
  const props = createMockProps({ recentDocuments });
138
- render(<ResourceDiscoveryPage {...props} />);
134
+ renderWithProviders(<ResourceDiscoveryPage {...props} />);
139
135
 
140
136
  expect(screen.getByText('Document 1')).toBeInTheDocument();
141
137
  expect(screen.getByText('Document 2')).toBeInTheDocument();
@@ -146,28 +142,28 @@ describe('ResourceDiscoveryPage', () => {
146
142
  const props = createMockProps({
147
143
  recentDocuments: [createMockResource('1', 'Document 1')],
148
144
  });
149
- render(<ResourceDiscoveryPage {...props} />);
145
+ renderWithProviders(<ResourceDiscoveryPage {...props} />);
150
146
 
151
147
  expect(screen.getByText('Recent Resources')).toBeInTheDocument();
152
148
  });
153
149
 
154
150
  it('shows empty state when no documents', () => {
155
151
  const props = createMockProps({ recentDocuments: [] });
156
- render(<ResourceDiscoveryPage {...props} />);
152
+ renderWithProviders(<ResourceDiscoveryPage {...props} />);
157
153
 
158
154
  expect(screen.getByText('No resources available')).toBeInTheDocument();
159
155
  });
160
156
 
161
157
  it('shows compose button in empty state', () => {
162
158
  const props = createMockProps({ recentDocuments: [] });
163
- render(<ResourceDiscoveryPage {...props} />);
159
+ renderWithProviders(<ResourceDiscoveryPage {...props} />);
164
160
 
165
161
  expect(screen.getByRole('button', { name: 'Compose First Resource' })).toBeInTheDocument();
166
162
  });
167
163
 
168
164
  it('calls onNavigateToCompose when compose button clicked', () => {
169
165
  const props = createMockProps({ recentDocuments: [] });
170
- render(<ResourceDiscoveryPage {...props} />);
166
+ renderWithProviders(<ResourceDiscoveryPage {...props} />);
171
167
 
172
168
  const button = screen.getByRole('button', { name: 'Compose First Resource' });
173
169
  fireEvent.click(button);
@@ -179,7 +175,7 @@ describe('ResourceDiscoveryPage', () => {
179
175
  describe('Search Functionality', () => {
180
176
  it('allows typing in search input', () => {
181
177
  const props = createMockProps();
182
- render(<ResourceDiscoveryPage {...props} />);
178
+ renderWithProviders(<ResourceDiscoveryPage {...props} />);
183
179
 
184
180
  const input = screen.getByPlaceholderText('Search resources...') as HTMLInputElement;
185
181
  fireEvent.change(input, { target: { value: 'test query' } });
@@ -189,14 +185,14 @@ describe('ResourceDiscoveryPage', () => {
189
185
 
190
186
  it('shows "Searching..." when isSearching is true', () => {
191
187
  const props = createMockProps({ isSearching: true });
192
- render(<ResourceDiscoveryPage {...props} />);
188
+ renderWithProviders(<ResourceDiscoveryPage {...props} />);
193
189
 
194
190
  expect(screen.getByRole('button', { name: 'Searching...' })).toBeInTheDocument();
195
191
  });
196
192
 
197
193
  it('disables search input when isSearching is true', () => {
198
194
  const props = createMockProps({ isSearching: true });
199
- render(<ResourceDiscoveryPage {...props} />);
195
+ renderWithProviders(<ResourceDiscoveryPage {...props} />);
200
196
 
201
197
  const input = screen.getByPlaceholderText('Search resources...') as HTMLInputElement;
202
198
  expect(input).toBeDisabled();
@@ -204,7 +200,7 @@ describe('ResourceDiscoveryPage', () => {
204
200
 
205
201
  it('disables search button when isSearching is true', () => {
206
202
  const props = createMockProps({ isSearching: true });
207
- render(<ResourceDiscoveryPage {...props} />);
203
+ renderWithProviders(<ResourceDiscoveryPage {...props} />);
208
204
 
209
205
  const button = screen.getByRole('button', { name: 'Searching...' });
210
206
  expect(button).toBeDisabled();
@@ -217,7 +213,7 @@ describe('ResourceDiscoveryPage', () => {
217
213
  ];
218
214
 
219
215
  const props = createMockProps({ searchDocuments });
220
- render(<ResourceDiscoveryPage {...props} />);
216
+ renderWithProviders(<ResourceDiscoveryPage {...props} />);
221
217
 
222
218
  // Type in search input to trigger search state
223
219
  const input = screen.getByPlaceholderText('Search resources...');
@@ -232,7 +228,7 @@ describe('ResourceDiscoveryPage', () => {
232
228
  searchDocuments: [],
233
229
  recentDocuments: [createMockResource('1', 'Recent Doc')],
234
230
  });
235
- render(<ResourceDiscoveryPage {...props} />);
231
+ renderWithProviders(<ResourceDiscoveryPage {...props} />);
236
232
 
237
233
  const input = screen.getByPlaceholderText('Search resources...');
238
234
  fireEvent.change(input, { target: { value: 'nonexistent' } });
@@ -248,7 +244,7 @@ describe('ResourceDiscoveryPage', () => {
248
244
  const props = createMockProps({
249
245
  entityTypes: ['Document', 'Article', 'Report'],
250
246
  });
251
- render(<ResourceDiscoveryPage {...props} />);
247
+ renderWithProviders(<ResourceDiscoveryPage {...props} />);
252
248
 
253
249
  expect(screen.getByText('Filter by type')).toBeInTheDocument();
254
250
  expect(screen.getByRole('button', { name: 'All' })).toBeInTheDocument();
@@ -266,7 +262,7 @@ describe('ResourceDiscoveryPage', () => {
266
262
  ],
267
263
  entityTypes: ['Document', 'Article'],
268
264
  });
269
- render(<ResourceDiscoveryPage {...props} />);
265
+ renderWithProviders(<ResourceDiscoveryPage {...props} />);
270
266
 
271
267
  // Initially all documents shown
272
268
  expect(screen.getByText('Doc 1')).toBeInTheDocument();
@@ -287,7 +283,7 @@ describe('ResourceDiscoveryPage', () => {
287
283
  recentDocuments: [createMockResource('1', 'Doc 1', ['Document'])],
288
284
  entityTypes: ['Document'],
289
285
  });
290
- render(<ResourceDiscoveryPage {...props} />);
286
+ renderWithProviders(<ResourceDiscoveryPage {...props} />);
291
287
 
292
288
  const documentButton = screen.getByRole('button', { name: 'Document' });
293
289
  fireEvent.click(documentButton);
@@ -303,7 +299,7 @@ describe('ResourceDiscoveryPage', () => {
303
299
  ],
304
300
  entityTypes: ['Document', 'Article'],
305
301
  });
306
- render(<ResourceDiscoveryPage {...props} />);
302
+ renderWithProviders(<ResourceDiscoveryPage {...props} />);
307
303
 
308
304
  // Filter by Document
309
305
  const documentButton = screen.getByRole('button', { name: 'Document' });
@@ -326,7 +322,7 @@ describe('ResourceDiscoveryPage', () => {
326
322
  const props = createMockProps({
327
323
  recentDocuments: [createMockResource('test-123', 'Test Document')],
328
324
  });
329
- render(<ResourceDiscoveryPage {...props} />);
325
+ renderWithProviders(<ResourceDiscoveryPage {...props} />);
330
326
 
331
327
  const card = screen.getByRole('button', { name: /Open resource: Test Document/ });
332
328
  fireEvent.click(card);
@@ -338,7 +334,7 @@ describe('ResourceDiscoveryPage', () => {
338
334
  describe('Toolbar Integration', () => {
339
335
  it('renders ToolbarPanels component', () => {
340
336
  const props = createMockProps();
341
- render(<ResourceDiscoveryPage {...props} />);
337
+ renderWithProviders(<ResourceDiscoveryPage {...props} />);
342
338
 
343
339
  expect(screen.getByTestId('toolbar-panels')).toBeInTheDocument();
344
340
  });
@@ -347,16 +343,14 @@ describe('ResourceDiscoveryPage', () => {
347
343
  const ToolbarPanels = vi.fn(() => <div data-testid="toolbar-panels" />);
348
344
  const props = createMockProps({
349
345
  theme: 'dark',
350
- onThemeChange: vi.fn(),
351
346
  ToolbarPanels,
352
347
  });
353
348
 
354
- render(<ResourceDiscoveryPage {...props} />);
349
+ renderWithProviders(<ResourceDiscoveryPage {...props} />);
355
350
 
356
351
  expect(ToolbarPanels).toHaveBeenCalledWith(
357
352
  expect.objectContaining({
358
353
  theme: 'dark',
359
- onThemeChange: expect.any(Function),
360
354
  }),
361
355
  expect.anything()
362
356
  );
@@ -5,10 +5,11 @@
5
5
  * All dependencies passed as props - no Next.js hooks!
6
6
  */
7
7
 
8
- import React, { useState, 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,
@@ -101,6 +96,10 @@ export function ResourceDiscoveryPage({
101
96
  { orientation: 'grid', cols: 2 } // 2 columns on medium+ screens
102
97
  );
103
98
 
99
+ // Store navigation callback in ref to avoid re-creating openResource
100
+ const onNavigateToResourceRef = useRef(onNavigateToResource);
101
+ onNavigateToResourceRef.current = onNavigateToResource;
102
+
104
103
  // Memoized callbacks
105
104
  const handleEntityTypeFilter = useCallback((entityType: string) => {
106
105
  setSelectedEntityType(entityType);
@@ -109,9 +108,9 @@ export function ResourceDiscoveryPage({
109
108
  const openResource = useCallback((resource: ResourceDescriptor) => {
110
109
  const resourceId = getResourceId(resource);
111
110
  if (resourceId) {
112
- onNavigateToResource(resourceId);
111
+ onNavigateToResourceRef.current(resourceId);
113
112
  }
114
- }, [onNavigateToResource]);
113
+ }, []);
115
114
 
116
115
  const handleSearchSubmit = useCallback((e: React.FormEvent) => {
117
116
  e.preventDefault();
@@ -267,16 +266,13 @@ export function ResourceDiscoveryPage({
267
266
  <ToolbarPanels
268
267
  activePanel={activePanel}
269
268
  theme={theme}
270
- onThemeChange={onThemeChange}
271
269
  showLineNumbers={showLineNumbers}
272
- onLineNumbersToggle={onLineNumbersToggle}
273
270
  />
274
271
 
275
272
  {/* Toolbar - Always visible on the right */}
276
273
  <Toolbar
277
274
  context="simple"
278
275
  activePanel={activePanel}
279
- onPanelToggle={onPanelToggle}
280
276
  />
281
277
  </div>
282
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
+ });