@semiont/react-ui 0.4.20 → 0.4.21
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/README.md +8 -5
- package/dist/{PdfAnnotationCanvas.client-CHDCGQBR.mjs → PdfAnnotationCanvas.client-6ZGFEN2N.mjs} +9 -13
- package/dist/PdfAnnotationCanvas.client-6ZGFEN2N.mjs.map +1 -0
- package/dist/TranslationManager-9Xj3MIWQ.d.mts +16 -0
- package/dist/chunk-KEDFYI6N.mjs +7788 -0
- package/dist/chunk-KEDFYI6N.mjs.map +1 -0
- package/dist/index.d.mts +171 -1140
- package/dist/index.mjs +3263 -13644
- package/dist/index.mjs.map +1 -1
- package/dist/test-utils.d.mts +46 -21
- package/dist/test-utils.mjs +2499 -87
- package/dist/test-utils.mjs.map +1 -1
- package/package.json +1 -2
- package/src/components/AnnotateReferencesProgressWidget.tsx +21 -28
- package/src/components/CodeMirrorRenderer.tsx +9 -11
- package/src/components/StatusDisplay.tsx +42 -16
- package/src/components/Toolbar.tsx +4 -4
- package/src/components/__tests__/AnnotateReferencesProgressWidget.test.tsx +34 -20
- package/src/components/__tests__/StatusDisplay.test.tsx +47 -64
- package/src/components/__tests__/Toolbar.test.tsx +4 -4
- package/src/components/annotation/AnnotateToolbar.tsx +8 -7
- package/src/components/annotation/__tests__/AnnotateToolbar.test.tsx +31 -77
- package/src/components/annotation-popups/__tests__/JsonLdView.test.tsx +0 -1
- package/src/components/image-annotation/AnnotationOverlay.tsx +12 -13
- package/src/components/image-annotation/SvgDrawingCanvas.tsx +7 -7
- package/src/components/modals/PermissionDeniedModal.tsx +11 -11
- package/src/components/modals/ReferenceWizardModal.tsx +14 -18
- package/src/components/modals/ResourceSearchModal.tsx +10 -6
- package/src/components/modals/SearchModal.tsx +10 -6
- package/src/components/modals/SessionExpiredModal.tsx +11 -11
- package/src/components/modals/__tests__/PermissionDeniedModal.test.tsx +7 -7
- package/src/components/modals/__tests__/ResourceSearchModal.test.tsx +10 -8
- package/src/components/modals/__tests__/SearchModal.search-wiring.test.tsx +10 -7
- package/src/components/modals/__tests__/SessionExpiredModal.test.tsx +5 -5
- package/src/components/navigation/CollapsibleResourceNavigation.tsx +10 -10
- package/src/components/navigation/ObservableLink.tsx +6 -6
- package/src/components/navigation/SimpleNavigation.tsx +4 -4
- package/src/components/navigation/__tests__/ObservableLink.test.tsx +4 -4
- package/src/components/navigation/__tests__/SimpleNavigation.test.tsx +4 -4
- package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +9 -11
- package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +0 -1
- package/src/components/resource/AnnotateView.tsx +7 -6
- package/src/components/resource/AnnotationHistory.tsx +9 -12
- package/src/components/resource/BrowseView.tsx +8 -7
- package/src/components/resource/ResourceViewer.tsx +17 -25
- package/src/components/resource/__tests__/AnnotationHistory.test.tsx +54 -192
- package/src/components/resource/__tests__/BrowseView.test.tsx +34 -83
- package/src/components/resource/__tests__/ResourceViewer.mode-switch.test.tsx +40 -31
- package/src/components/resource/panels/AssessmentEntry.tsx +5 -4
- package/src/components/resource/panels/AssessmentPanel.tsx +19 -15
- package/src/components/resource/panels/AssistSection.tsx +11 -13
- package/src/components/resource/panels/CollaborationPanel.tsx +29 -7
- package/src/components/resource/panels/CommentEntry.tsx +5 -4
- package/src/components/resource/panels/CommentsPanel.tsx +9 -11
- package/src/components/resource/panels/HighlightEntry.tsx +5 -4
- package/src/components/resource/panels/HighlightPanel.tsx +10 -11
- package/src/components/resource/panels/ReferenceEntry.tsx +8 -8
- package/src/components/resource/panels/ReferencesPanel.tsx +13 -12
- package/src/components/resource/panels/ResourceInfoPanel.tsx +7 -6
- package/src/components/resource/panels/TagEntry.tsx +5 -4
- package/src/components/resource/panels/TaggingPanel.tsx +10 -16
- package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +3 -2
- package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +18 -52
- package/src/components/resource/panels/__tests__/CollaborationPanel.test.tsx +51 -20
- package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +18 -56
- package/src/components/resource/panels/__tests__/HighlightPanel.annotationProgress.test.tsx +0 -1
- package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +4 -5
- package/src/components/resource/panels/__tests__/ReferencesPanel.observable-flow.test.tsx +153 -0
- package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +51 -106
- package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +15 -47
- package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +15 -47
- package/src/components/settings/SettingsPanel.tsx +8 -8
- package/src/components/settings/__tests__/SettingsPanel.test.tsx +12 -12
- package/src/features/admin-devops/components/AdminDevOpsPage.tsx +1 -1
- package/src/features/admin-exchange/components/AdminExchangePage.tsx +1 -1
- package/src/features/admin-exchange/components/ImportCard.tsx +2 -6
- package/src/features/admin-security/components/AdminSecurityPage.tsx +1 -1
- package/src/features/admin-users/components/AdminUsersPage.tsx +1 -1
- package/src/features/moderate-entity-tags/components/EntityTagsPage.tsx +1 -1
- package/src/features/moderate-recent/components/RecentDocumentsPage.tsx +1 -1
- package/src/features/moderate-tag-schemas/components/TagSchemasPage.tsx +1 -1
- package/src/features/moderation-linked-data/components/LinkedDataPage.tsx +1 -1
- package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +5 -3
- package/src/features/resource-compose/components/ResourceComposePage.tsx +5 -22
- package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +4 -3
- package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +1 -1
- package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +38 -45
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +123 -192
- package/dist/KnowledgeBaseSessionContext-BNNunwzO.d.mts +0 -175
- package/dist/PdfAnnotationCanvas.client-CHDCGQBR.mjs.map +0 -1
- package/dist/chunk-OZICDVH7.mjs +0 -62
- package/dist/chunk-OZICDVH7.mjs.map +0 -1
- package/dist/chunk-R4CCMFJH.mjs +0 -877
- package/dist/chunk-R4CCMFJH.mjs.map +0 -1
- package/dist/chunk-VN5NY4SN.mjs +0 -200
- package/dist/chunk-VN5NY4SN.mjs.map +0 -1
- package/src/components/modals/ProposeEntitiesModal.tsx +0 -179
- package/src/components/modals/__tests__/ProposeEntitiesModal.test.tsx +0 -129
- package/src/features/resource-viewer/__tests__/AnnotationCreationPending.test.tsx +0 -323
- package/src/features/resource-viewer/__tests__/AnnotationDeletionIntegration.test.tsx +0 -245
- package/src/features/resource-viewer/__tests__/AnnotationProgressDismissal.test.tsx +0 -303
- package/src/features/resource-viewer/__tests__/BindFlowIntegration.test.tsx +0 -150
- package/src/features/resource-viewer/__tests__/DetectionFlowBug.test.tsx +0 -243
- package/src/features/resource-viewer/__tests__/DetectionFlowIntegration.test.tsx +0 -383
- package/src/features/resource-viewer/__tests__/ResourceMutations.test.tsx +0 -299
- package/src/features/resource-viewer/__tests__/ToastNotifications.test.tsx +0 -186
- package/src/features/resource-viewer/__tests__/YieldFlowIntegration.test.tsx +0 -429
- 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
|
-
});
|