@semiont/react-ui 0.4.20 → 0.4.22

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 (125) hide show
  1. package/README.md +8 -5
  2. package/dist/{PdfAnnotationCanvas.client-CHDCGQBR.mjs → PdfAnnotationCanvas.client-5QESNO5H.mjs} +13 -16
  3. package/dist/PdfAnnotationCanvas.client-5QESNO5H.mjs.map +1 -0
  4. package/dist/TranslationManager-9Xj3MIWQ.d.mts +16 -0
  5. package/dist/chunk-4NOUO3W6.mjs +7788 -0
  6. package/dist/chunk-4NOUO3W6.mjs.map +1 -0
  7. package/dist/index.d.mts +212 -1206
  8. package/dist/index.mjs +3332 -13712
  9. package/dist/index.mjs.map +1 -1
  10. package/dist/test-utils.d.mts +48 -21
  11. package/dist/test-utils.mjs +2505 -87
  12. package/dist/test-utils.mjs.map +1 -1
  13. package/package.json +2 -2
  14. package/src/components/AnnotateReferencesProgressWidget.tsx +21 -28
  15. package/src/components/CodeMirrorRenderer.tsx +12 -12
  16. package/src/components/LiveRegion.tsx +1 -2
  17. package/src/components/StatusDisplay.tsx +42 -16
  18. package/src/components/Toolbar.tsx +4 -4
  19. package/src/components/__tests__/AnnotateReferencesProgressWidget.test.tsx +34 -20
  20. package/src/components/__tests__/StatusDisplay.test.tsx +50 -65
  21. package/src/components/__tests__/Toolbar.test.tsx +4 -4
  22. package/src/components/annotation/AnnotateToolbar.tsx +8 -9
  23. package/src/components/annotation/__tests__/AnnotateToolbar.test.tsx +31 -77
  24. package/src/components/annotation-popups/JsonLdView.tsx +1 -2
  25. package/src/components/annotation-popups/__tests__/JsonLdView.test.tsx +1 -2
  26. package/src/components/image-annotation/AnnotationOverlay.tsx +15 -18
  27. package/src/components/image-annotation/SvgDrawingCanvas.tsx +12 -17
  28. package/src/components/modals/ConfigureGenerationStep.tsx +1 -2
  29. package/src/components/modals/PermissionDeniedModal.tsx +11 -11
  30. package/src/components/modals/ReferenceWizardModal.tsx +14 -18
  31. package/src/components/modals/ResourceSearchModal.tsx +12 -8
  32. package/src/components/modals/SearchModal.tsx +11 -6
  33. package/src/components/modals/SearchResultsStep.tsx +1 -3
  34. package/src/components/modals/SessionExpiredModal.tsx +11 -11
  35. package/src/components/modals/__tests__/PermissionDeniedModal.test.tsx +7 -7
  36. package/src/components/modals/__tests__/ResourceSearchModal.test.tsx +10 -8
  37. package/src/components/modals/__tests__/SearchModal.accessibility.test.tsx +6 -2
  38. package/src/components/modals/__tests__/SearchModal.basic.test.tsx +6 -2
  39. package/src/components/modals/__tests__/SearchModal.keyboard.test.tsx +6 -2
  40. package/src/components/modals/__tests__/SearchModal.search-wiring.test.tsx +10 -7
  41. package/src/components/modals/__tests__/SearchModal.visual.test.tsx +6 -2
  42. package/src/components/modals/__tests__/SessionExpiredModal.test.tsx +5 -5
  43. package/src/components/navigation/CollapsibleResourceNavigation.tsx +10 -10
  44. package/src/components/navigation/ObservableLink.tsx +6 -6
  45. package/src/components/navigation/SimpleNavigation.tsx +4 -4
  46. package/src/components/navigation/__tests__/ObservableLink.test.tsx +4 -4
  47. package/src/components/navigation/__tests__/SimpleNavigation.test.tsx +4 -4
  48. package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +15 -18
  49. package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +1 -2
  50. package/src/components/resource/AnnotateView.tsx +8 -10
  51. package/src/components/resource/AnnotationHistory.tsx +9 -12
  52. package/src/components/resource/BrowseView.tsx +11 -8
  53. package/src/components/resource/ResourceViewer.tsx +22 -34
  54. package/src/components/resource/__tests__/AnnotationHistory.test.tsx +54 -192
  55. package/src/components/resource/__tests__/BrowseView.test.tsx +38 -87
  56. package/src/components/resource/__tests__/ResourceViewer.mode-switch.test.tsx +41 -31
  57. package/src/components/resource/__tests__/event-formatting.test.ts +6 -2
  58. package/src/components/resource/event-formatting.ts +2 -3
  59. package/src/components/resource/panels/AssessmentEntry.tsx +7 -8
  60. package/src/components/resource/panels/AssessmentPanel.tsx +21 -17
  61. package/src/components/resource/panels/AssistSection.tsx +15 -21
  62. package/src/components/resource/panels/CollaborationPanel.tsx +29 -7
  63. package/src/components/resource/panels/CommentEntry.tsx +7 -8
  64. package/src/components/resource/panels/CommentsPanel.tsx +11 -13
  65. package/src/components/resource/panels/HighlightEntry.tsx +7 -8
  66. package/src/components/resource/panels/HighlightPanel.tsx +12 -13
  67. package/src/components/resource/panels/ReferenceEntry.tsx +13 -15
  68. package/src/components/resource/panels/ReferencesPanel.tsx +17 -19
  69. package/src/components/resource/panels/ResourceInfoPanel.tsx +8 -7
  70. package/src/components/resource/panels/StatisticsPanel.tsx +2 -3
  71. package/src/components/resource/panels/TagEntry.tsx +7 -8
  72. package/src/components/resource/panels/TaggingPanel.tsx +14 -23
  73. package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +4 -3
  74. package/src/components/resource/panels/__tests__/AssessmentEntry.test.tsx +4 -4
  75. package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +22 -57
  76. package/src/components/resource/panels/__tests__/CollaborationPanel.test.tsx +51 -20
  77. package/src/components/resource/panels/__tests__/CommentEntry.test.tsx +4 -4
  78. package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +22 -61
  79. package/src/components/resource/panels/__tests__/HighlightEntry.test.tsx +4 -4
  80. package/src/components/resource/panels/__tests__/HighlightPanel.annotationProgress.test.tsx +1 -2
  81. package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +7 -8
  82. package/src/components/resource/panels/__tests__/ReferencesPanel.observable-flow.test.tsx +153 -0
  83. package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +51 -106
  84. package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +28 -53
  85. package/src/components/resource/panels/__tests__/StatisticsPanel.test.tsx +3 -3
  86. package/src/components/resource/panels/__tests__/TagEntry.test.tsx +4 -4
  87. package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +19 -52
  88. package/src/components/settings/SettingsPanel.tsx +9 -9
  89. package/src/components/settings/__tests__/SettingsPanel.test.tsx +15 -15
  90. package/src/features/admin-devops/components/AdminDevOpsPage.tsx +1 -2
  91. package/src/features/admin-exchange/components/AdminExchangePage.tsx +1 -1
  92. package/src/features/admin-exchange/components/ImportCard.tsx +2 -7
  93. package/src/features/admin-security/components/AdminSecurityPage.tsx +1 -2
  94. package/src/features/admin-users/components/AdminUsersPage.tsx +1 -1
  95. package/src/features/moderate-entity-tags/components/EntityTagsPage.tsx +1 -2
  96. package/src/features/moderate-recent/components/RecentDocumentsPage.tsx +1 -2
  97. package/src/features/moderate-tag-schemas/components/TagSchemasPage.tsx +1 -1
  98. package/src/features/moderation-linked-data/components/LinkedDataPage.tsx +1 -1
  99. package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +5 -3
  100. package/src/features/resource-compose/components/ResourceComposePage.tsx +6 -22
  101. package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +4 -3
  102. package/src/features/resource-discovery/components/ResourceCard.tsx +1 -2
  103. package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +3 -4
  104. package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +37 -45
  105. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +129 -197
  106. package/dist/KnowledgeBaseSessionContext-BNNunwzO.d.mts +0 -175
  107. package/dist/PdfAnnotationCanvas.client-CHDCGQBR.mjs.map +0 -1
  108. package/dist/chunk-OZICDVH7.mjs +0 -62
  109. package/dist/chunk-OZICDVH7.mjs.map +0 -1
  110. package/dist/chunk-R4CCMFJH.mjs +0 -877
  111. package/dist/chunk-R4CCMFJH.mjs.map +0 -1
  112. package/dist/chunk-VN5NY4SN.mjs +0 -200
  113. package/dist/chunk-VN5NY4SN.mjs.map +0 -1
  114. package/src/components/modals/ProposeEntitiesModal.tsx +0 -179
  115. package/src/components/modals/__tests__/ProposeEntitiesModal.test.tsx +0 -129
  116. package/src/features/resource-viewer/__tests__/AnnotationCreationPending.test.tsx +0 -323
  117. package/src/features/resource-viewer/__tests__/AnnotationDeletionIntegration.test.tsx +0 -245
  118. package/src/features/resource-viewer/__tests__/AnnotationProgressDismissal.test.tsx +0 -303
  119. package/src/features/resource-viewer/__tests__/BindFlowIntegration.test.tsx +0 -150
  120. package/src/features/resource-viewer/__tests__/DetectionFlowBug.test.tsx +0 -243
  121. package/src/features/resource-viewer/__tests__/DetectionFlowIntegration.test.tsx +0 -383
  122. package/src/features/resource-viewer/__tests__/ResourceMutations.test.tsx +0 -299
  123. package/src/features/resource-viewer/__tests__/ToastNotifications.test.tsx +0 -186
  124. package/src/features/resource-viewer/__tests__/YieldFlowIntegration.test.tsx +0 -429
  125. package/src/features/resource-viewer/__tests__/annotation-progress-flow.test.tsx +0 -348
@@ -1,129 +0,0 @@
1
- import { describe, it, expect, beforeEach, vi } from 'vitest';
2
- import React from 'react';
3
- import { screen, fireEvent } from '@testing-library/react';
4
- import { renderWithProviders } from '../../../test-utils';
5
- import '@testing-library/jest-dom';
6
- import { ProposeEntitiesModal } from '../ProposeEntitiesModal';
7
-
8
- // Mock HeadlessUI to avoid jsdom OOM issues
9
- vi.mock('@headlessui/react', () => ({
10
- Dialog: ({ children, onClose, ...props }: any) => <div role="dialog" {...props}>{typeof children === 'function' ? children({ open: true }) : children}</div>,
11
- DialogPanel: ({ children, ...props }: any) => <div {...props}>{children}</div>,
12
- DialogTitle: ({ children, ...props }: any) => <h2 {...props}>{children}</h2>,
13
- DialogDescription: ({ children, ...props }: any) => <p {...props}>{children}</p>,
14
- Transition: ({ show, children }: any) => show ? <>{children}</> : null,
15
- TransitionChild: ({ children }: any) => <>{children}</>,
16
- }));
17
-
18
- // Stable entity types array to avoid infinite re-render loops
19
- const mockEntityTypes = ['Person', 'Organization', 'Location'];
20
- const stableQueryResult = { data: { entityTypes: mockEntityTypes } };
21
-
22
- vi.mock('../../../lib/api-hooks', () => ({
23
- useEntityTypes: vi.fn(() => ({
24
- list: {
25
- useQuery: () => stableQueryResult,
26
- },
27
- })),
28
- }));
29
-
30
- describe('ProposeEntitiesModal', () => {
31
- const defaultProps = {
32
- isOpen: true,
33
- onConfirm: vi.fn(),
34
- onCancel: vi.fn(),
35
- };
36
-
37
- beforeEach(() => {
38
- vi.clearAllMocks();
39
- sessionStorage.clear();
40
- });
41
-
42
- it('renders modal title when open', () => {
43
- renderWithProviders(<ProposeEntitiesModal {...defaultProps} />);
44
- expect(screen.getByText('Detect Entity References')).toBeInTheDocument();
45
- });
46
-
47
- it('does not render when closed', () => {
48
- renderWithProviders(
49
- <ProposeEntitiesModal {...defaultProps} isOpen={false} />
50
- );
51
- expect(screen.queryByText('Detect Entity References')).not.toBeInTheDocument();
52
- });
53
-
54
- it('renders available entity type buttons', () => {
55
- renderWithProviders(<ProposeEntitiesModal {...defaultProps} />);
56
- expect(screen.getByText('Person')).toBeInTheDocument();
57
- expect(screen.getByText('Organization')).toBeInTheDocument();
58
- expect(screen.getByText('Location')).toBeInTheDocument();
59
- });
60
-
61
- it('toggles entity type selection on click', () => {
62
- renderWithProviders(<ProposeEntitiesModal {...defaultProps} />);
63
-
64
- fireEvent.click(screen.getByText('Person'));
65
- expect(screen.getByText('1 type selected')).toBeInTheDocument();
66
-
67
- fireEvent.click(screen.getByText('Organization'));
68
- expect(screen.getByText('2 types selected')).toBeInTheDocument();
69
-
70
- // Deselect
71
- fireEvent.click(screen.getByText('Person'));
72
- expect(screen.getByText('1 type selected')).toBeInTheDocument();
73
- });
74
-
75
- it('disables confirm button when no types selected', () => {
76
- renderWithProviders(<ProposeEntitiesModal {...defaultProps} />);
77
- const buttons = screen.getAllByRole('button');
78
- const confirmButton = buttons.find(b => b.textContent?.includes('Detect Entity'));
79
- expect(confirmButton).toBeDisabled();
80
- });
81
-
82
- it('enables confirm button when types are selected', () => {
83
- renderWithProviders(<ProposeEntitiesModal {...defaultProps} />);
84
- fireEvent.click(screen.getByText('Person'));
85
-
86
- const buttons = screen.getAllByRole('button');
87
- const confirmButton = buttons.find(b => b.textContent?.includes('Detect Entity'));
88
- expect(confirmButton).not.toBeDisabled();
89
- });
90
-
91
- it('calls onConfirm with selected types', () => {
92
- const onConfirm = vi.fn();
93
- renderWithProviders(
94
- <ProposeEntitiesModal {...defaultProps} onConfirm={onConfirm} />
95
- );
96
-
97
- fireEvent.click(screen.getByText('Person'));
98
- fireEvent.click(screen.getByText('Location'));
99
-
100
- const buttons = screen.getAllByRole('button');
101
- const confirmButton = buttons.find(b => b.textContent?.includes('Detect Entity'));
102
- fireEvent.click(confirmButton!);
103
-
104
- expect(onConfirm).toHaveBeenCalledWith(['Person', 'Location']);
105
- });
106
-
107
- it('calls onCancel when cancel button is clicked', () => {
108
- const onCancel = vi.fn();
109
- renderWithProviders(
110
- <ProposeEntitiesModal {...defaultProps} onCancel={onCancel} />
111
- );
112
- fireEvent.click(screen.getByText('Cancel'));
113
- expect(onCancel).toHaveBeenCalled();
114
- });
115
-
116
- it('saves preferences to sessionStorage on confirm', () => {
117
- renderWithProviders(<ProposeEntitiesModal {...defaultProps} />);
118
-
119
- fireEvent.click(screen.getByText('Person'));
120
-
121
- const buttons = screen.getAllByRole('button');
122
- const confirmButton = buttons.find(b => b.textContent?.includes('Detect Entity'));
123
- fireEvent.click(confirmButton!);
124
-
125
- expect(sessionStorage.getItem('userPreferredEntityTypes')).toBe(
126
- JSON.stringify(['Person'])
127
- );
128
- });
129
- });
@@ -1,323 +0,0 @@
1
- /**
2
- * Regression test: pendingAnnotation cleared after mark:submit succeeds
3
- *
4
- * Bug: handleAnnotationCreate in useMarkFlow called the API and emitted
5
- * mark:created, but never called setPendingAnnotation(null). The pending
6
- * creation form (e.g. "Create Reference", "Save" assessment) remained visible
7
- * after the user clicked the confirm button.
8
- *
9
- * Fix: setPendingAnnotation(null) added in handleAnnotationCreate on success,
10
- * before emitting mark:created.
11
- *
12
- * This test covers all four motivations that have a pending form:
13
- * - linking (ReferencesPanel: "Create Reference" button)
14
- * - assessing (AssessmentPanel: "Save" button)
15
- * - commenting (CommentsPanel: "Save" button)
16
- * - tagging (TaggingPanel: category selection)
17
- */
18
-
19
- import React from 'react';
20
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
21
- import { render, screen, waitFor } from '@testing-library/react';
22
- import { act } from 'react';
23
- import { useMarkFlow } from '../../../hooks/useMarkFlow';
24
- import { EventBusProvider, useEventBus } from '../../../contexts/EventBusContext';
25
- import { ApiClientProvider } from '../../../contexts/ApiClientContext';
26
- import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
27
- import { SemiontApiClient } from '@semiont/api-client';
28
- import { resourceId } from '@semiont/core';
29
- import type { Emitter } from 'mitt';
30
- import type { EventMap } from '@semiont/core';
31
-
32
- // Mock Toast module to prevent "useToast must be used within a ToastProvider" errors
33
- vi.mock('../../../components/Toast', () => ({
34
- useToast: () => ({
35
- showSuccess: vi.fn(),
36
- showError: vi.fn(),
37
- showInfo: vi.fn(),
38
- showWarning: vi.fn(),
39
- }),
40
- }));
41
- import type { Motivation, Selector } from '@semiont/core';
42
-
43
- const TEST_URI = resourceId('test-resource');
44
-
45
- const MOCK_ANNOTATION = {
46
- id: 'new-1',
47
- type: 'Annotation',
48
- motivation: 'linking' as Motivation,
49
- target: { source: TEST_URI },
50
- body: [],
51
- };
52
-
53
- const TEXT_SELECTOR: Selector = {
54
- type: 'TextQuoteSelector',
55
- exact: 'some selected text',
56
- };
57
-
58
- const SVG_SELECTOR: Selector = {
59
- type: 'SvgSelector',
60
- value: '<rect x="10" y="20" width="100" height="50"/>',
61
- };
62
-
63
- // ─── Helper ───────────────────────────────────────────────────────────────────
64
-
65
- function renderDetectionFlow(testUri: string) {
66
- let eventBusInstance: Emitter<EventMap>;
67
-
68
- function EventBusCapture() {
69
- eventBusInstance = useEventBus();
70
- return null;
71
- }
72
-
73
- function DetectionFlowHarness() {
74
- const { pendingAnnotation } = useMarkFlow(testUri as any);
75
- return (
76
- <div>
77
- <div data-testid="pending-motivation">
78
- {pendingAnnotation ? pendingAnnotation.motivation : 'none'}
79
- </div>
80
- </div>
81
- );
82
- }
83
-
84
- render(
85
- <EventBusProvider>
86
- <AuthTokenProvider token={null}>
87
- <ApiClientProvider baseUrl="http://localhost:4000">
88
- <EventBusCapture />
89
- <DetectionFlowHarness />
90
- </ApiClientProvider>
91
- </AuthTokenProvider>
92
- </EventBusProvider>
93
- );
94
-
95
- return {
96
- getEventBus: () => eventBusInstance,
97
- emit: <K extends keyof EventMap>(event: K, payload: EventMap[K]) => {
98
- eventBusInstance.get(event).next(payload);
99
- },
100
- };
101
- }
102
-
103
- // ─── Tests ────────────────────────────────────────────────────────────────────
104
-
105
- describe('Annotation creation clears pendingAnnotation', () => {
106
- let markAnnotationSpy: ReturnType<typeof vi.spyOn>;
107
-
108
- beforeEach(() => {
109
- vi.clearAllMocks();
110
- markAnnotationSpy = vi
111
- .spyOn(SemiontApiClient.prototype, 'markAnnotation')
112
- .mockResolvedValue({ annotationId: MOCK_ANNOTATION.id } as any);
113
- });
114
-
115
- afterEach(() => {
116
- vi.restoreAllMocks();
117
- });
118
-
119
- it('clears pendingAnnotation after creating a reference (linking)', async () => {
120
- const { emit } = renderDetectionFlow(TEST_URI);
121
-
122
- // Set a pending annotation
123
- act(() => {
124
- emit('mark:requested', { selector: TEXT_SELECTOR, motivation: 'linking' });
125
- });
126
-
127
- await waitFor(() => {
128
- expect(screen.getByTestId('pending-motivation')).toHaveTextContent('linking');
129
- });
130
-
131
- // Emit mark:submit(what ReferencesPanel does when user clicks "Create Reference")
132
- await act(async () => {
133
- emit('mark:submit', {
134
- motivation: 'linking',
135
- selector: TEXT_SELECTOR,
136
- body: [{ type: 'TextualBody', value: 'Person', purpose: 'tagging' }],
137
- });
138
- });
139
-
140
- // pendingAnnotation must be cleared
141
- await waitFor(() => {
142
- expect(screen.getByTestId('pending-motivation')).toHaveTextContent('none');
143
- });
144
-
145
- expect(markAnnotationSpy).toHaveBeenCalledTimes(1);
146
- });
147
-
148
- it('clears pendingAnnotation after creating an assessment (assessing)', async () => {
149
- const { emit } = renderDetectionFlow(TEST_URI);
150
-
151
- act(() => {
152
- emit('mark:requested', { selector: SVG_SELECTOR, motivation: 'assessing' });
153
- });
154
-
155
- await waitFor(() => {
156
- expect(screen.getByTestId('pending-motivation')).toHaveTextContent('assessing');
157
- });
158
-
159
- await act(async () => {
160
- emit('mark:submit', {
161
- motivation: 'assessing',
162
- selector: SVG_SELECTOR,
163
- body: [{ type: 'TextualBody', value: 'Looks good', purpose: 'assessing' }],
164
- });
165
- });
166
-
167
- await waitFor(() => {
168
- expect(screen.getByTestId('pending-motivation')).toHaveTextContent('none');
169
- });
170
- });
171
-
172
- it('clears pendingAnnotation after creating an assessment with empty body (optional text)', async () => {
173
- const { emit } = renderDetectionFlow(TEST_URI);
174
-
175
- act(() => {
176
- emit('mark:requested', { selector: SVG_SELECTOR, motivation: 'assessing' });
177
- });
178
-
179
- await waitFor(() => {
180
- expect(screen.getByTestId('pending-motivation')).toHaveTextContent('assessing');
181
- });
182
-
183
- // Empty body is valid for assessments
184
- await act(async () => {
185
- emit('mark:submit', {
186
- motivation: 'assessing',
187
- selector: SVG_SELECTOR,
188
- body: [],
189
- });
190
- });
191
-
192
- await waitFor(() => {
193
- expect(screen.getByTestId('pending-motivation')).toHaveTextContent('none');
194
- });
195
- });
196
-
197
- it('clears pendingAnnotation after creating a comment (commenting)', async () => {
198
- const { emit } = renderDetectionFlow(TEST_URI);
199
-
200
- act(() => {
201
- emit('mark:requested', { selector: TEXT_SELECTOR, motivation: 'commenting' });
202
- });
203
-
204
- await waitFor(() => {
205
- expect(screen.getByTestId('pending-motivation')).toHaveTextContent('commenting');
206
- });
207
-
208
- await act(async () => {
209
- emit('mark:submit', {
210
- motivation: 'commenting',
211
- selector: TEXT_SELECTOR,
212
- body: [{ type: 'TextualBody', value: 'Great point', purpose: 'commenting' }],
213
- });
214
- });
215
-
216
- await waitFor(() => {
217
- expect(screen.getByTestId('pending-motivation')).toHaveTextContent('none');
218
- });
219
- });
220
-
221
- it('clears pendingAnnotation after creating a tag (tagging)', async () => {
222
- const { emit } = renderDetectionFlow(TEST_URI);
223
-
224
- act(() => {
225
- emit('mark:requested', { selector: SVG_SELECTOR, motivation: 'tagging' });
226
- });
227
-
228
- await waitFor(() => {
229
- expect(screen.getByTestId('pending-motivation')).toHaveTextContent('tagging');
230
- });
231
-
232
- await act(async () => {
233
- emit('mark:submit', {
234
- motivation: 'tagging',
235
- selector: SVG_SELECTOR,
236
- body: [{ type: 'TextualBody', value: 'concept:trust', purpose: 'tagging' }],
237
- });
238
- });
239
-
240
- await waitFor(() => {
241
- expect(screen.getByTestId('pending-motivation')).toHaveTextContent('none');
242
- });
243
- });
244
-
245
- it('emits mark:created after successful creation', async () => {
246
- const { emit, getEventBus } = renderDetectionFlow(TEST_URI);
247
-
248
- const createdListener = vi.fn();
249
- // Set listener after first render so eventBus is captured
250
- await waitFor(() => expect(getEventBus()).toBeDefined());
251
- const subscription = getEventBus().get('mark:create-ok').subscribe(createdListener);
252
-
253
- act(() => {
254
- emit('mark:requested', { selector: TEXT_SELECTOR, motivation: 'linking' });
255
- });
256
-
257
- await act(async () => {
258
- emit('mark:submit', {
259
- motivation: 'linking',
260
- selector: TEXT_SELECTOR,
261
- body: [],
262
- });
263
- });
264
-
265
- await waitFor(() => {
266
- expect(createdListener).toHaveBeenCalledTimes(1);
267
- expect(createdListener).toHaveBeenCalledWith({ annotationId: 'new-1' });
268
- });
269
-
270
- subscription.unsubscribe();
271
- });
272
-
273
- it('does NOT clear pendingAnnotation if API call fails', async () => {
274
- markAnnotationSpy.mockRejectedValueOnce(new Error('Network error'));
275
-
276
- const { emit } = renderDetectionFlow(TEST_URI);
277
-
278
- act(() => {
279
- emit('mark:requested', { selector: TEXT_SELECTOR, motivation: 'linking' });
280
- });
281
-
282
- await waitFor(() => {
283
- expect(screen.getByTestId('pending-motivation')).toHaveTextContent('linking');
284
- });
285
-
286
- await act(async () => {
287
- emit('mark:submit', {
288
- motivation: 'linking',
289
- selector: TEXT_SELECTOR,
290
- body: [],
291
- });
292
- });
293
-
294
- // Give async rejection time to settle
295
- await waitFor(() => {
296
- // pending should remain — user can retry or cancel
297
- expect(screen.getByTestId('pending-motivation')).toHaveTextContent('linking');
298
- });
299
- });
300
-
301
- it('clears pendingAnnotation on cancel (mark:cancel-pending)', async () => {
302
- const { emit } = renderDetectionFlow(TEST_URI);
303
-
304
- act(() => {
305
- emit('mark:requested', { selector: TEXT_SELECTOR, motivation: 'assessing' });
306
- });
307
-
308
- await waitFor(() => {
309
- expect(screen.getByTestId('pending-motivation')).toHaveTextContent('assessing');
310
- });
311
-
312
- act(() => {
313
- emit('mark:cancel-pending', undefined);
314
- });
315
-
316
- await waitFor(() => {
317
- expect(screen.getByTestId('pending-motivation')).toHaveTextContent('none');
318
- });
319
-
320
- // API should NOT have been called on cancel
321
- expect(markAnnotationSpy).not.toHaveBeenCalled();
322
- });
323
- });
@@ -1,245 +0,0 @@
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
- * - useMarkFlow (REAL) — single registration point for useBindFlow
8
- * - useEventSubscriptions (REAL)
9
- *
10
- * This test focuses on ARCHITECTURE and EVENT WIRING:
11
- * - Verifies deletion API called exactly ONCE (catches duplicate subscriptions)
12
- * - Tests event propagation through the event bus
13
- * - Validates success/failure event emissions
14
- * - Ensures auth token is passed correctly
15
- *
16
- * CRITICAL: This test prevents regressions where:
17
- * - Multiple deletion paths exist (event-driven vs direct)
18
- * - useBindFlow called in more than one hook (causes duplicate subscriptions)
19
- * - Auth token missing from API calls (401 errors)
20
- *
21
- * ARCHITECTURE: useBindFlow is called ONLY in useMarkFlow.
22
- * useMarkFlow handles all detection state (manual annotation selection
23
- * and AI-driven SSE detection) plus all API operations via useBindFlow.
24
- */
25
-
26
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
27
- import { render, waitFor } from '@testing-library/react';
28
- import { act } from 'react';
29
- import { useMarkFlow } from '../../../hooks/useMarkFlow';
30
- import { useStoreTokenSync } from '../../../hooks/useStoreTokenSync';
31
- import { EventBusProvider, useEventBus } from '../../../contexts/EventBusContext';
32
-
33
- // Mock Toast module to prevent "useToast must be used within a ToastProvider" errors
34
- vi.mock('../../../components/Toast', () => ({
35
- useToast: () => ({
36
- showSuccess: vi.fn(),
37
- showError: vi.fn(),
38
- showInfo: vi.fn(),
39
- showWarning: vi.fn(),
40
- }),
41
- }));
42
- import { ApiClientProvider } from '../../../contexts/ApiClientContext';
43
- import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
44
- import { SemiontApiClient } from '@semiont/api-client';
45
- import { resourceId, accessToken, annotationId as toAnnotationId } from '@semiont/core';
46
-
47
- describe('Annotation Deletion - Feature Integration', () => {
48
- let deleteAnnotationSpy: ReturnType<typeof vi.fn>;
49
- const testId = resourceId('test-resource');
50
- const testToken = 'test-token-123';
51
- const testBaseUrl = 'http://localhost:4000';
52
-
53
- beforeEach(() => {
54
- vi.clearAllMocks();
55
-
56
- // Mock the deleteAnnotation method on SemiontApiClient prototype
57
- deleteAnnotationSpy = vi.fn().mockResolvedValue({ success: true });
58
- vi.spyOn(SemiontApiClient.prototype, 'deleteAnnotation').mockImplementation(deleteAnnotationSpy);
59
- });
60
-
61
- afterEach(() => {
62
- vi.restoreAllMocks();
63
- });
64
-
65
- /**
66
- * Helper to render the annotation flow with real providers
67
- */
68
- function renderAnnotationFlow() {
69
- let eventBusInstance: ReturnType<typeof useEventBus> | null = null;
70
-
71
- function TestComponent() {
72
- eventBusInstance = useEventBus();
73
- useStoreTokenSync(); // Syncs auth token to namespace getToken
74
- useMarkFlow(testId);
75
- return null;
76
- }
77
-
78
- render(
79
- <AuthTokenProvider token={testToken}>
80
- <EventBusProvider>
81
- <ApiClientProvider baseUrl={testBaseUrl}>
82
- <TestComponent />
83
- </ApiClientProvider>
84
- </EventBusProvider>
85
- </AuthTokenProvider>
86
- );
87
-
88
- return {
89
- emitDelete: (annotationId: string) => {
90
- act(() => {
91
- eventBusInstance!.get('mark:delete').next({ annotationId: toAnnotationId(annotationId) });
92
- });
93
- },
94
- eventBus: eventBusInstance!,
95
- };
96
- }
97
-
98
- it('should call deleteAnnotation API exactly ONCE when mark:deleteevent is emitted', async () => {
99
- const { emitDelete } = renderAnnotationFlow();
100
- const annotationId = 'annotation-123';
101
-
102
- // Trigger deletion via event bus (how UI triggers it)
103
- emitDelete(annotationId);
104
-
105
- // CRITICAL ASSERTION: API called exactly once (not twice!)
106
- // This would FAIL if there were competing deletion paths
107
- await waitFor(() => {
108
- expect(deleteAnnotationSpy).toHaveBeenCalledTimes(1);
109
- });
110
-
111
- // Verify correct parameters (resourceId, annotationId, opts)
112
- expect(deleteAnnotationSpy).toHaveBeenCalledWith(
113
- 'test-resource',
114
- expect.stringContaining(annotationId),
115
- expect.objectContaining({
116
- auth: accessToken(testToken),
117
- })
118
- );
119
- });
120
-
121
- it('should pass auth token to API call (prevents 401 errors)', async () => {
122
- const { emitDelete } = renderAnnotationFlow();
123
-
124
- emitDelete('annotation-456');
125
-
126
- await waitFor(() => {
127
- expect(deleteAnnotationSpy).toHaveBeenCalled();
128
- });
129
-
130
- // CRITICAL: Auth token must be present (3rd arg is opts)
131
- const callArgs = deleteAnnotationSpy.mock.calls[0];
132
- expect(callArgs[2]).toHaveProperty('auth');
133
- expect(callArgs[2].auth).toBe(accessToken(testToken));
134
- });
135
-
136
- it('should emit mark:deleted event on successful deletion', async () => {
137
- const { emitDelete, eventBus } = renderAnnotationFlow();
138
- const deletedListener = vi.fn();
139
-
140
- // Subscribe to success event
141
- eventBus.get('mark:delete-ok').subscribe(deletedListener);
142
-
143
- emitDelete('annotation-789');
144
-
145
- // Wait for API call to complete
146
- await waitFor(() => {
147
- expect(deleteAnnotationSpy).toHaveBeenCalled();
148
- });
149
-
150
- // Verify success event was emitted
151
- await waitFor(() => {
152
- expect(deletedListener).toHaveBeenCalledWith({
153
- annotationId: 'annotation-789',
154
- });
155
- });
156
- });
157
-
158
- it('should emit mark:delete-failed event on API error', async () => {
159
- // Make API call fail
160
- deleteAnnotationSpy.mockRejectedValue(new Error('Network error'));
161
-
162
- const { emitDelete, eventBus } = renderAnnotationFlow();
163
- const failedListener = vi.fn();
164
-
165
- // Subscribe to failure event
166
- eventBus.get('mark:delete-failed').subscribe(failedListener);
167
-
168
- emitDelete('annotation-error');
169
-
170
- // Wait for API call to be attempted
171
- await waitFor(() => {
172
- expect(deleteAnnotationSpy).toHaveBeenCalled();
173
- });
174
-
175
- // Verify failure event was emitted
176
- await waitFor(() => {
177
- expect(failedListener).toHaveBeenCalledWith({
178
- message: expect.any(String),
179
- });
180
- });
181
- });
182
-
183
- it('should handle multiple deletions in sequence without duplicate API calls', async () => {
184
- const { emitDelete } = renderAnnotationFlow();
185
-
186
- // Delete first annotation
187
- emitDelete('annotation-1');
188
-
189
- await waitFor(() => {
190
- expect(deleteAnnotationSpy).toHaveBeenCalledTimes(1);
191
- });
192
-
193
- // Delete second annotation
194
- emitDelete('annotation-2');
195
-
196
- await waitFor(() => {
197
- expect(deleteAnnotationSpy).toHaveBeenCalledTimes(2);
198
- });
199
-
200
- // Verify each call had correct annotation ID (2nd arg)
201
- expect(deleteAnnotationSpy.mock.calls[0][1]).toContain('annotation-1');
202
- expect(deleteAnnotationSpy.mock.calls[1][1]).toContain('annotation-2');
203
- });
204
-
205
- it('ARCHITECTURE: useBindFlow is called in useMarkFlow (single registration point)', async () => {
206
- /**
207
- * This test validates that there's only ONE event-driven deletion path:
208
- * - useMarkFlow calls useBindFlow (the single registration point)
209
- * - useBindFlow subscribes to annotation:delete
210
- *
211
- * If this test fails with 2 API calls, it means useBindFlow was added
212
- * to a second hook, causing duplicate subscriptions (ARCHITECTURE VIOLATION).
213
- */
214
-
215
- const { emitDelete } = renderAnnotationFlow();
216
-
217
- emitDelete('architecture-test');
218
-
219
- // Single API call = single subscription = correct architecture
220
- await waitFor(() => {
221
- expect(deleteAnnotationSpy).toHaveBeenCalledTimes(1);
222
- });
223
- });
224
-
225
- it('REGRESSION: No direct deleteAnnotation function in ResourceAnnotationsContext', () => {
226
- /**
227
- * This test prevents regression to the old pattern where
228
- * ResourceAnnotationsContext had a deleteAnnotation function
229
- * that bypassed the event bus.
230
- *
231
- * The correct pattern is event-driven only:
232
- * - UI emits mark:deleteevent
233
- * - useBindFlow handles it
234
- * - No direct function calls
235
- */
236
-
237
- // This would fail to compile if deleteAnnotation was added back to context
238
- // Type-level enforcement via TypeScript
239
- const { emitDelete } = renderAnnotationFlow();
240
- emitDelete('regression-test');
241
-
242
- // Deletion still works via events
243
- expect(deleteAnnotationSpy).toHaveBeenCalled();
244
- });
245
- });