@semiont/react-ui 0.2.33-build.79 → 0.2.33-build.80
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/EventBusContext-7GvDyO0d.d.mts +414 -0
- package/dist/{PdfAnnotationCanvas.client-ADC4FFSE.mjs → PdfAnnotationCanvas.client-RAJRPQLU.mjs} +42 -27
- package/dist/PdfAnnotationCanvas.client-RAJRPQLU.mjs.map +1 -0
- package/dist/{ar-EMHEHPCJ.mjs → ar-4ZEORRW2.mjs} +7 -4
- package/dist/ar-4ZEORRW2.mjs.map +1 -0
- package/dist/{bn-OVCI4F6X.mjs → bn-SEDE5BQJ.mjs} +7 -4
- package/dist/bn-SEDE5BQJ.mjs.map +1 -0
- package/dist/{chunk-LIHZTECW.mjs → chunk-D7NBW4RV.mjs} +7 -4
- package/dist/chunk-D7NBW4RV.mjs.map +1 -0
- package/dist/{chunk-JZIO2A3B.mjs → chunk-ZR4ZV2LY.mjs} +206 -146
- package/dist/chunk-ZR4ZV2LY.mjs.map +1 -0
- package/dist/{cs-FAN66Q2F.mjs → cs-7W4WF5WD.mjs} +7 -4
- package/dist/cs-7W4WF5WD.mjs.map +1 -0
- package/dist/{da-YBBIHI2O.mjs → da-75XGBCBK.mjs} +7 -4
- package/dist/da-75XGBCBK.mjs.map +1 -0
- package/dist/{de-MAYU33LB.mjs → de-ODJVFLHM.mjs} +7 -4
- package/dist/de-ODJVFLHM.mjs.map +1 -0
- package/dist/{el-MKGSWN4O.mjs → el-C4PM4WB3.mjs} +7 -4
- package/dist/el-C4PM4WB3.mjs.map +1 -0
- package/dist/{en-DDLIXJCU.mjs → en-KJCJQ4OO.mjs} +2 -2
- package/dist/{es-52LHUWJD.mjs → es-WD33R7QL.mjs} +7 -4
- package/dist/es-WD33R7QL.mjs.map +1 -0
- package/dist/{fa-FJICRANB.mjs → fa-2BP6V56P.mjs} +7 -4
- package/dist/fa-2BP6V56P.mjs.map +1 -0
- package/dist/{fi-O455XFCR.mjs → fi-USRRW24J.mjs} +7 -4
- package/dist/fi-USRRW24J.mjs.map +1 -0
- package/dist/{fr-TXIXHOOE.mjs → fr-EC5S6WVF.mjs} +7 -4
- package/dist/fr-EC5S6WVF.mjs.map +1 -0
- package/dist/{he-JBSOX5IN.mjs → he-7TBVIKAA.mjs} +7 -4
- package/dist/he-7TBVIKAA.mjs.map +1 -0
- package/dist/{hi-KGHI3XVT.mjs → hi-FO4VIZLA.mjs} +7 -4
- package/dist/hi-FO4VIZLA.mjs.map +1 -0
- package/dist/{id-5OCPPZLO.mjs → id-7U7GGVWY.mjs} +7 -4
- package/dist/id-7U7GGVWY.mjs.map +1 -0
- package/dist/index.css +123 -85
- package/dist/index.css.map +1 -1
- package/dist/index.d.mts +645 -471
- package/dist/index.mjs +3461 -3025
- package/dist/index.mjs.map +1 -1
- package/dist/{it-PNBBZSM2.mjs → it-Y4OPL6I2.mjs} +7 -4
- package/dist/it-Y4OPL6I2.mjs.map +1 -0
- package/dist/{ja-LDD7R3TJ.mjs → ja-PK7SQL55.mjs} +7 -4
- package/dist/ja-PK7SQL55.mjs.map +1 -0
- package/dist/{ko-F47ZDEY3.mjs → ko-L25PXMYD.mjs} +7 -4
- package/dist/ko-L25PXMYD.mjs.map +1 -0
- package/dist/{ms-Z7LMXJWL.mjs → ms-STH777QM.mjs} +7 -4
- package/dist/ms-STH777QM.mjs.map +1 -0
- package/dist/{nl-6SJFBPJ3.mjs → nl-Y7LECDDR.mjs} +7 -4
- package/dist/nl-Y7LECDDR.mjs.map +1 -0
- package/dist/{no-YXPBPSGF.mjs → no-KEKCEWU6.mjs} +7 -4
- package/dist/no-KEKCEWU6.mjs.map +1 -0
- package/dist/{pl-P4AZ2QME.mjs → pl-7A7OC75O.mjs} +7 -4
- package/dist/pl-7A7OC75O.mjs.map +1 -0
- package/dist/{pt-LHWUS6U6.mjs → pt-35HTM7RA.mjs} +7 -4
- package/dist/pt-35HTM7RA.mjs.map +1 -0
- package/dist/{ro-EA5J2ZON.mjs → ro-VAWL5KQA.mjs} +7 -4
- package/dist/ro-VAWL5KQA.mjs.map +1 -0
- package/dist/{sv-DATBS3UQ.mjs → sv-7ZK5EQEB.mjs} +7 -4
- package/dist/sv-7ZK5EQEB.mjs.map +1 -0
- package/dist/test-utils.d.mts +18 -8
- package/dist/test-utils.mjs +36 -14
- package/dist/test-utils.mjs.map +1 -1
- package/dist/{th-WTFJRWPT.mjs → th-UDWZ4X34.mjs} +7 -4
- package/dist/th-UDWZ4X34.mjs.map +1 -0
- package/dist/{tr-IKO3RXOX.mjs → tr-4WMPK3UX.mjs} +7 -4
- package/dist/tr-4WMPK3UX.mjs.map +1 -0
- package/dist/{uk-CF6CTTRK.mjs → uk-SSLASQYJ.mjs} +7 -4
- package/dist/uk-SSLASQYJ.mjs.map +1 -0
- package/dist/{vi-AJLTXPZQ.mjs → vi-IF42Z5PU.mjs} +7 -4
- package/dist/vi-IF42Z5PU.mjs.map +1 -0
- package/dist/{zh-U3ORHHYH.mjs → zh-HRQTNTAI.mjs} +7 -4
- package/dist/zh-HRQTNTAI.mjs.map +1 -0
- package/package.json +3 -1
- package/src/components/CodeMirrorRenderer.tsx +66 -93
- package/src/components/DetectionProgressWidget.tsx +16 -5
- package/src/components/LiveRegion.tsx +18 -18
- package/src/components/ResizeHandle.tsx +10 -4
- package/src/components/SessionTimer.tsx +2 -2
- package/src/components/Toolbar.tsx +18 -9
- package/src/components/__tests__/SessionTimer.test.tsx +9 -9
- package/src/components/annotation/AnnotateToolbar.tsx +17 -15
- package/src/components/annotation/__tests__/AnnotateToolbar.test.tsx +165 -63
- package/src/components/annotation/annotation-entries.css +10 -0
- package/src/components/annotation-popups/JsonLdView.tsx +8 -2
- package/src/components/image-annotation/AnnotationOverlay.tsx +42 -22
- package/src/components/image-annotation/SvgDrawingCanvas.tsx +27 -30
- package/src/components/layout/__tests__/LeftSidebar.test.tsx +12 -33
- package/src/components/layout/__tests__/PageLayout.test.tsx +37 -32
- package/src/components/layout/__tests__/UnifiedHeader.test.tsx +21 -40
- package/src/components/modals/ResourceSearchModal.tsx +2 -2
- package/src/components/modals/SearchModal.tsx +1 -1
- package/src/components/navigation/CollapsibleResourceNavigation.tsx +14 -9
- package/src/components/navigation/NavigationTabs.css +36 -24
- package/src/components/navigation/ObservableLink.tsx +91 -0
- package/src/components/navigation/SimpleNavigation.tsx +20 -16
- package/src/components/navigation/SortableResourceTab.tsx +11 -5
- package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +51 -26
- package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +28 -22
- package/src/components/resource/AnnotateView.tsx +64 -134
- package/src/components/resource/BrowseView.tsx +86 -166
- package/src/components/resource/HistoryEvent.tsx +13 -7
- package/src/components/resource/ResourceViewer.tsx +122 -264
- package/src/components/resource/__tests__/BrowseView.test.tsx +631 -0
- package/src/components/resource/__tests__/ResourceViewer.mode-switch.test.tsx +231 -0
- package/src/components/resource/panels/AssessmentEntry.tsx +25 -33
- package/src/components/resource/panels/AssessmentPanel.tsx +106 -28
- package/src/components/resource/panels/CommentEntry.tsx +38 -32
- package/src/components/resource/panels/CommentsPanel.tsx +121 -28
- package/src/components/resource/panels/DetectSection.css +36 -1
- package/src/components/resource/panels/DetectSection.tsx +38 -10
- package/src/components/resource/panels/HighlightEntry.tsx +25 -33
- package/src/components/resource/panels/HighlightPanel.tsx +100 -25
- package/src/components/resource/panels/ReferenceEntry.tsx +61 -75
- package/src/components/resource/panels/ReferencesPanel.tsx +134 -42
- package/src/components/resource/panels/ResourceInfoPanel.tsx +47 -48
- package/src/components/resource/panels/TagEntry.tsx +25 -33
- package/src/components/resource/panels/TaggingPanel.tsx +119 -30
- package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +30 -92
- package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +129 -110
- package/src/components/resource/panels/__tests__/CommentEntry.test.tsx +86 -78
- package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +144 -149
- package/src/components/resource/panels/__tests__/DetectSection.test.tsx +480 -0
- package/src/components/resource/panels/__tests__/HighlightPanel.detectionProgress.test.tsx +362 -0
- package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +226 -111
- package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +117 -61
- package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +128 -106
- package/src/components/settings/SettingsPanel.tsx +15 -12
- package/src/features/admin-devops/__tests__/AdminDevOpsPage.test.tsx +1 -46
- package/src/features/admin-devops/components/AdminDevOpsPage.tsx +0 -9
- package/src/features/admin-security/__tests__/AdminSecurityPage.test.tsx +0 -3
- package/src/features/admin-security/components/AdminSecurityPage.tsx +0 -9
- package/src/features/admin-users/__tests__/AdminUsersPage.test.tsx +0 -3
- package/src/features/admin-users/components/AdminUsersPage.tsx +0 -9
- package/src/features/moderate-entity-tags/__tests__/EntityTagsPage.test.tsx +0 -3
- package/src/features/moderate-entity-tags/components/EntityTagsPage.tsx +1 -9
- package/src/features/moderate-recent/__tests__/RecentDocumentsPage.test.tsx +0 -32
- package/src/features/moderate-recent/components/RecentDocumentsPage.tsx +1 -9
- package/src/features/moderate-tag-schemas/__tests__/TagSchemasPage.test.tsx +0 -32
- package/src/features/moderate-tag-schemas/components/TagSchemasPage.tsx +1 -9
- package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +51 -54
- package/src/features/resource-compose/components/ResourceComposePage.tsx +3 -13
- package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +39 -45
- package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +9 -13
- package/src/features/resource-viewer/__tests__/AnnotationDeletionIntegration.test.tsx +231 -0
- package/src/features/resource-viewer/__tests__/DetectionFlowBug.test.tsx +234 -0
- package/src/features/resource-viewer/__tests__/DetectionFlowIntegration.test.tsx +388 -0
- package/src/features/resource-viewer/__tests__/DetectionProgressDismissal.test.tsx +318 -0
- package/src/features/resource-viewer/__tests__/GenerationFlowIntegration.test.tsx +504 -0
- package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +135 -88
- package/src/features/resource-viewer/__tests__/detection-progress-flow.test.tsx +322 -0
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +308 -528
- package/translations/ar.json +6 -3
- package/translations/bn.json +6 -3
- package/translations/cs.json +6 -3
- package/translations/da.json +6 -3
- package/translations/de.json +6 -3
- package/translations/el.json +6 -3
- package/translations/en.json +6 -3
- package/translations/es.json +6 -3
- package/translations/fa.json +6 -3
- package/translations/fi.json +6 -3
- package/translations/fr.json +6 -3
- package/translations/he.json +6 -3
- package/translations/hi.json +6 -3
- package/translations/id.json +6 -3
- package/translations/it.json +6 -3
- package/translations/ja.json +6 -3
- package/translations/ko.json +6 -3
- package/translations/ms.json +6 -3
- package/translations/nl.json +6 -3
- package/translations/no.json +6 -3
- package/translations/pl.json +6 -3
- package/translations/pt.json +6 -3
- package/translations/ro.json +6 -3
- package/translations/sv.json +6 -3
- package/translations/th.json +6 -3
- package/translations/tr.json +6 -3
- package/translations/uk.json +6 -3
- package/translations/vi.json +6 -3
- package/translations/zh.json +6 -3
- package/dist/PdfAnnotationCanvas.client-ADC4FFSE.mjs.map +0 -1
- package/dist/TranslationManager-Co_5fSxl.d.mts +0 -118
- package/dist/ar-EMHEHPCJ.mjs.map +0 -1
- package/dist/bn-OVCI4F6X.mjs.map +0 -1
- package/dist/chunk-JZIO2A3B.mjs.map +0 -1
- package/dist/chunk-LIHZTECW.mjs.map +0 -1
- package/dist/cs-FAN66Q2F.mjs.map +0 -1
- package/dist/da-YBBIHI2O.mjs.map +0 -1
- package/dist/de-MAYU33LB.mjs.map +0 -1
- package/dist/el-MKGSWN4O.mjs.map +0 -1
- package/dist/es-52LHUWJD.mjs.map +0 -1
- package/dist/fa-FJICRANB.mjs.map +0 -1
- package/dist/fi-O455XFCR.mjs.map +0 -1
- package/dist/fr-TXIXHOOE.mjs.map +0 -1
- package/dist/he-JBSOX5IN.mjs.map +0 -1
- package/dist/hi-KGHI3XVT.mjs.map +0 -1
- package/dist/id-5OCPPZLO.mjs.map +0 -1
- package/dist/it-PNBBZSM2.mjs.map +0 -1
- package/dist/ja-LDD7R3TJ.mjs.map +0 -1
- package/dist/ko-F47ZDEY3.mjs.map +0 -1
- package/dist/ms-Z7LMXJWL.mjs.map +0 -1
- package/dist/nl-6SJFBPJ3.mjs.map +0 -1
- package/dist/no-YXPBPSGF.mjs.map +0 -1
- package/dist/pl-P4AZ2QME.mjs.map +0 -1
- package/dist/pt-LHWUS6U6.mjs.map +0 -1
- package/dist/ro-EA5J2ZON.mjs.map +0 -1
- package/dist/sv-DATBS3UQ.mjs.map +0 -1
- package/dist/th-WTFJRWPT.mjs.map +0 -1
- package/dist/tr-IKO3RXOX.mjs.map +0 -1
- package/dist/uk-CF6CTTRK.mjs.map +0 -1
- package/dist/vi-AJLTXPZQ.mjs.map +0 -1
- package/dist/zh-U3ORHHYH.mjs.map +0 -1
- /package/dist/{en-DDLIXJCU.mjs.map → en-KJCJQ4OO.mjs.map} +0 -0
|
@@ -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
|
|
13
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
101
|
+
const { container } = renderWithProviders(<ResourceDiscoveryPage {...props} />);
|
|
108
102
|
|
|
109
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
111
|
+
onNavigateToResourceRef.current(resourceId);
|
|
113
112
|
}
|
|
114
|
-
}, [
|
|
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
|
+
});
|