@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.
- package/README.md +8 -5
- package/dist/{PdfAnnotationCanvas.client-CHDCGQBR.mjs → PdfAnnotationCanvas.client-5QESNO5H.mjs} +13 -16
- package/dist/PdfAnnotationCanvas.client-5QESNO5H.mjs.map +1 -0
- package/dist/TranslationManager-9Xj3MIWQ.d.mts +16 -0
- package/dist/chunk-4NOUO3W6.mjs +7788 -0
- package/dist/chunk-4NOUO3W6.mjs.map +1 -0
- package/dist/index.d.mts +212 -1206
- package/dist/index.mjs +3332 -13712
- package/dist/index.mjs.map +1 -1
- package/dist/test-utils.d.mts +48 -21
- package/dist/test-utils.mjs +2505 -87
- package/dist/test-utils.mjs.map +1 -1
- package/package.json +2 -2
- package/src/components/AnnotateReferencesProgressWidget.tsx +21 -28
- package/src/components/CodeMirrorRenderer.tsx +12 -12
- package/src/components/LiveRegion.tsx +1 -2
- 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 +50 -65
- package/src/components/__tests__/Toolbar.test.tsx +4 -4
- package/src/components/annotation/AnnotateToolbar.tsx +8 -9
- package/src/components/annotation/__tests__/AnnotateToolbar.test.tsx +31 -77
- package/src/components/annotation-popups/JsonLdView.tsx +1 -2
- package/src/components/annotation-popups/__tests__/JsonLdView.test.tsx +1 -2
- package/src/components/image-annotation/AnnotationOverlay.tsx +15 -18
- package/src/components/image-annotation/SvgDrawingCanvas.tsx +12 -17
- package/src/components/modals/ConfigureGenerationStep.tsx +1 -2
- package/src/components/modals/PermissionDeniedModal.tsx +11 -11
- package/src/components/modals/ReferenceWizardModal.tsx +14 -18
- package/src/components/modals/ResourceSearchModal.tsx +12 -8
- package/src/components/modals/SearchModal.tsx +11 -6
- package/src/components/modals/SearchResultsStep.tsx +1 -3
- 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.accessibility.test.tsx +6 -2
- package/src/components/modals/__tests__/SearchModal.basic.test.tsx +6 -2
- package/src/components/modals/__tests__/SearchModal.keyboard.test.tsx +6 -2
- package/src/components/modals/__tests__/SearchModal.search-wiring.test.tsx +10 -7
- package/src/components/modals/__tests__/SearchModal.visual.test.tsx +6 -2
- 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 +15 -18
- package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +1 -2
- package/src/components/resource/AnnotateView.tsx +8 -10
- package/src/components/resource/AnnotationHistory.tsx +9 -12
- package/src/components/resource/BrowseView.tsx +11 -8
- package/src/components/resource/ResourceViewer.tsx +22 -34
- package/src/components/resource/__tests__/AnnotationHistory.test.tsx +54 -192
- package/src/components/resource/__tests__/BrowseView.test.tsx +38 -87
- package/src/components/resource/__tests__/ResourceViewer.mode-switch.test.tsx +41 -31
- package/src/components/resource/__tests__/event-formatting.test.ts +6 -2
- package/src/components/resource/event-formatting.ts +2 -3
- package/src/components/resource/panels/AssessmentEntry.tsx +7 -8
- package/src/components/resource/panels/AssessmentPanel.tsx +21 -17
- package/src/components/resource/panels/AssistSection.tsx +15 -21
- package/src/components/resource/panels/CollaborationPanel.tsx +29 -7
- package/src/components/resource/panels/CommentEntry.tsx +7 -8
- package/src/components/resource/panels/CommentsPanel.tsx +11 -13
- package/src/components/resource/panels/HighlightEntry.tsx +7 -8
- package/src/components/resource/panels/HighlightPanel.tsx +12 -13
- package/src/components/resource/panels/ReferenceEntry.tsx +13 -15
- package/src/components/resource/panels/ReferencesPanel.tsx +17 -19
- package/src/components/resource/panels/ResourceInfoPanel.tsx +8 -7
- package/src/components/resource/panels/StatisticsPanel.tsx +2 -3
- package/src/components/resource/panels/TagEntry.tsx +7 -8
- package/src/components/resource/panels/TaggingPanel.tsx +14 -23
- package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +4 -3
- package/src/components/resource/panels/__tests__/AssessmentEntry.test.tsx +4 -4
- package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +22 -57
- package/src/components/resource/panels/__tests__/CollaborationPanel.test.tsx +51 -20
- package/src/components/resource/panels/__tests__/CommentEntry.test.tsx +4 -4
- package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +22 -61
- package/src/components/resource/panels/__tests__/HighlightEntry.test.tsx +4 -4
- package/src/components/resource/panels/__tests__/HighlightPanel.annotationProgress.test.tsx +1 -2
- package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +7 -8
- 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 +28 -53
- package/src/components/resource/panels/__tests__/StatisticsPanel.test.tsx +3 -3
- package/src/components/resource/panels/__tests__/TagEntry.test.tsx +4 -4
- package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +19 -52
- package/src/components/settings/SettingsPanel.tsx +9 -9
- package/src/components/settings/__tests__/SettingsPanel.test.tsx +15 -15
- package/src/features/admin-devops/components/AdminDevOpsPage.tsx +1 -2
- package/src/features/admin-exchange/components/AdminExchangePage.tsx +1 -1
- package/src/features/admin-exchange/components/ImportCard.tsx +2 -7
- package/src/features/admin-security/components/AdminSecurityPage.tsx +1 -2
- package/src/features/admin-users/components/AdminUsersPage.tsx +1 -1
- package/src/features/moderate-entity-tags/components/EntityTagsPage.tsx +1 -2
- package/src/features/moderate-recent/components/RecentDocumentsPage.tsx +1 -2
- 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 +6 -22
- package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +4 -3
- package/src/features/resource-discovery/components/ResourceCard.tsx +1 -2
- package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +3 -4
- package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +37 -45
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +129 -197
- 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
|
@@ -36,7 +36,10 @@ vi.mock('../../../../contexts/TranslationContext', () => ({
|
|
|
36
36
|
|
|
37
37
|
describe('CollaborationPanel Component', () => {
|
|
38
38
|
const defaultProps = {
|
|
39
|
-
|
|
39
|
+
// `degraded` is the "sustained disconnect" state — matches the
|
|
40
|
+
// pre-state-machine `isConnected: false` default, which was always
|
|
41
|
+
// used in tests to mean "the UI should show Disconnected."
|
|
42
|
+
state: 'degraded' as const,
|
|
40
43
|
eventCount: 0,
|
|
41
44
|
};
|
|
42
45
|
|
|
@@ -72,19 +75,19 @@ describe('CollaborationPanel Component', () => {
|
|
|
72
75
|
|
|
73
76
|
describe('Connection Status', () => {
|
|
74
77
|
it('should show disconnected status when not connected', () => {
|
|
75
|
-
render(<CollaborationPanel {...defaultProps}
|
|
78
|
+
render(<CollaborationPanel {...defaultProps} state="degraded" />);
|
|
76
79
|
|
|
77
80
|
expect(screen.getByText('Disconnected')).toBeInTheDocument();
|
|
78
81
|
});
|
|
79
82
|
|
|
80
83
|
it('should show live status when connected', () => {
|
|
81
|
-
render(<CollaborationPanel {...defaultProps}
|
|
84
|
+
render(<CollaborationPanel {...defaultProps} state="open" />);
|
|
82
85
|
|
|
83
86
|
expect(screen.getByText('Live')).toBeInTheDocument();
|
|
84
87
|
});
|
|
85
88
|
|
|
86
89
|
it('should show indicator when disconnected', () => {
|
|
87
|
-
const { container } = render(<CollaborationPanel {...defaultProps}
|
|
90
|
+
const { container } = render(<CollaborationPanel {...defaultProps} state="degraded" />);
|
|
88
91
|
|
|
89
92
|
const indicator = container.querySelector('.semiont-collaboration-panel__dot');
|
|
90
93
|
expect(indicator).toBeInTheDocument();
|
|
@@ -92,7 +95,7 @@ describe('CollaborationPanel Component', () => {
|
|
|
92
95
|
});
|
|
93
96
|
|
|
94
97
|
it('should show indicator when connected', () => {
|
|
95
|
-
const { container } = render(<CollaborationPanel {...defaultProps}
|
|
98
|
+
const { container } = render(<CollaborationPanel {...defaultProps} state="open" />);
|
|
96
99
|
|
|
97
100
|
const indicator = container.querySelector('.semiont-collaboration-panel__dot');
|
|
98
101
|
expect(indicator).toBeInTheDocument();
|
|
@@ -100,7 +103,7 @@ describe('CollaborationPanel Component', () => {
|
|
|
100
103
|
});
|
|
101
104
|
|
|
102
105
|
it('should use appropriate status text for disconnected state', () => {
|
|
103
|
-
render(<CollaborationPanel {...defaultProps}
|
|
106
|
+
render(<CollaborationPanel {...defaultProps} state="degraded" />);
|
|
104
107
|
|
|
105
108
|
const statusText = screen.getByText('Disconnected');
|
|
106
109
|
expect(statusText).toHaveClass('semiont-collaboration-panel__status-text');
|
|
@@ -108,35 +111,63 @@ describe('CollaborationPanel Component', () => {
|
|
|
108
111
|
});
|
|
109
112
|
|
|
110
113
|
it('should use appropriate status text for connected state', () => {
|
|
111
|
-
render(<CollaborationPanel {...defaultProps}
|
|
114
|
+
render(<CollaborationPanel {...defaultProps} state="open" />);
|
|
112
115
|
|
|
113
116
|
const statusText = screen.getByText('Live');
|
|
114
117
|
expect(statusText).toHaveClass('semiont-collaboration-panel__status-text');
|
|
115
118
|
expect(statusText).toHaveAttribute('data-connected', 'true');
|
|
116
119
|
});
|
|
120
|
+
|
|
121
|
+
// ── State-machine aware cases (post-CONNECTION-STATE) ─────────────
|
|
122
|
+
// These exercise the core reason CONNECTION-STATE exists: brief
|
|
123
|
+
// reconnect/connect cycles must NOT flash "Disconnected", or
|
|
124
|
+
// Strict-Mode mount churn makes the UI lie.
|
|
125
|
+
|
|
126
|
+
it('shows Live during brief `reconnecting` (does not alarm on churn)', () => {
|
|
127
|
+
render(<CollaborationPanel {...defaultProps} state="reconnecting" />);
|
|
128
|
+
expect(screen.getByText('Live')).toBeInTheDocument();
|
|
129
|
+
expect(screen.queryByText('Disconnected')).not.toBeInTheDocument();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('shows Live during `connecting` and `initial`', () => {
|
|
133
|
+
const { rerender } = render(<CollaborationPanel {...defaultProps} state="connecting" />);
|
|
134
|
+
expect(screen.getByText('Live')).toBeInTheDocument();
|
|
135
|
+
rerender(<CollaborationPanel {...defaultProps} state="initial" />);
|
|
136
|
+
expect(screen.getByText('Live')).toBeInTheDocument();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('shows Disconnected on `degraded` (sustained disconnect)', () => {
|
|
140
|
+
render(<CollaborationPanel {...defaultProps} state="degraded" />);
|
|
141
|
+
expect(screen.getByText('Disconnected')).toBeInTheDocument();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('shows Disconnected on `closed` (terminal)', () => {
|
|
145
|
+
render(<CollaborationPanel {...defaultProps} state="closed" />);
|
|
146
|
+
expect(screen.getByText('Disconnected')).toBeInTheDocument();
|
|
147
|
+
});
|
|
117
148
|
});
|
|
118
149
|
|
|
119
150
|
describe('Event Count', () => {
|
|
120
151
|
it('should not show event count when zero', () => {
|
|
121
|
-
render(<CollaborationPanel {...defaultProps}
|
|
152
|
+
render(<CollaborationPanel {...defaultProps} state="open" eventCount={0} />);
|
|
122
153
|
|
|
123
154
|
expect(screen.queryByText(/event/i)).not.toBeInTheDocument();
|
|
124
155
|
});
|
|
125
156
|
|
|
126
157
|
it('should show event count when connected and greater than zero', () => {
|
|
127
|
-
render(<CollaborationPanel {...defaultProps}
|
|
158
|
+
render(<CollaborationPanel {...defaultProps} state="open" eventCount={5} />);
|
|
128
159
|
|
|
129
160
|
expect(screen.getByText(/event/i)).toBeInTheDocument();
|
|
130
161
|
});
|
|
131
162
|
|
|
132
163
|
it('should not show event count when disconnected', () => {
|
|
133
|
-
render(<CollaborationPanel {...defaultProps}
|
|
164
|
+
render(<CollaborationPanel {...defaultProps} state="degraded" eventCount={5} />);
|
|
134
165
|
|
|
135
166
|
expect(screen.queryByText(/event/i)).not.toBeInTheDocument();
|
|
136
167
|
});
|
|
137
168
|
|
|
138
169
|
it('should display correct event count', () => {
|
|
139
|
-
render(<CollaborationPanel {...defaultProps}
|
|
170
|
+
render(<CollaborationPanel {...defaultProps} state="open" eventCount={42} />);
|
|
140
171
|
|
|
141
172
|
// The translation will have ${count} in it
|
|
142
173
|
expect(screen.getByText(/event/i)).toBeInTheDocument();
|
|
@@ -267,13 +298,13 @@ describe('CollaborationPanel Component', () => {
|
|
|
267
298
|
|
|
268
299
|
describe('Real-time Status Messages', () => {
|
|
269
300
|
it('should show "real-time active" when connected', () => {
|
|
270
|
-
render(<CollaborationPanel {...defaultProps}
|
|
301
|
+
render(<CollaborationPanel {...defaultProps} state="open" />);
|
|
271
302
|
|
|
272
303
|
expect(screen.getByText('Real-time synchronization active')).toBeInTheDocument();
|
|
273
304
|
});
|
|
274
305
|
|
|
275
306
|
it('should show "reconnecting" when disconnected', () => {
|
|
276
|
-
render(<CollaborationPanel {...defaultProps}
|
|
307
|
+
render(<CollaborationPanel {...defaultProps} state="degraded" />);
|
|
277
308
|
|
|
278
309
|
expect(screen.getByText('Reconnecting...')).toBeInTheDocument();
|
|
279
310
|
});
|
|
@@ -281,11 +312,11 @@ describe('CollaborationPanel Component', () => {
|
|
|
281
312
|
|
|
282
313
|
describe('Dynamic Updates', () => {
|
|
283
314
|
it('should update when connection status changes', () => {
|
|
284
|
-
const { rerender } = render(<CollaborationPanel {...defaultProps}
|
|
315
|
+
const { rerender } = render(<CollaborationPanel {...defaultProps} state="degraded" />);
|
|
285
316
|
|
|
286
317
|
expect(screen.getByText('Disconnected')).toBeInTheDocument();
|
|
287
318
|
|
|
288
|
-
rerender(<CollaborationPanel {...defaultProps}
|
|
319
|
+
rerender(<CollaborationPanel {...defaultProps} state="open" />);
|
|
289
320
|
|
|
290
321
|
expect(screen.getByText('Live')).toBeInTheDocument();
|
|
291
322
|
expect(screen.queryByText('Disconnected')).not.toBeInTheDocument();
|
|
@@ -293,12 +324,12 @@ describe('CollaborationPanel Component', () => {
|
|
|
293
324
|
|
|
294
325
|
it('should update when event count changes', () => {
|
|
295
326
|
const { rerender } = render(
|
|
296
|
-
<CollaborationPanel {...defaultProps}
|
|
327
|
+
<CollaborationPanel {...defaultProps} state="open" eventCount={5} />
|
|
297
328
|
);
|
|
298
329
|
|
|
299
330
|
expect(screen.getByText(/event/i)).toBeInTheDocument();
|
|
300
331
|
|
|
301
|
-
rerender(<CollaborationPanel {...defaultProps}
|
|
332
|
+
rerender(<CollaborationPanel {...defaultProps} state="open" eventCount={10} />);
|
|
302
333
|
|
|
303
334
|
expect(screen.getByText(/event/i)).toBeInTheDocument();
|
|
304
335
|
});
|
|
@@ -371,7 +402,7 @@ describe('CollaborationPanel Component', () => {
|
|
|
371
402
|
it('should handle very large event counts', () => {
|
|
372
403
|
expect(() => {
|
|
373
404
|
render(
|
|
374
|
-
<CollaborationPanel {...defaultProps}
|
|
405
|
+
<CollaborationPanel {...defaultProps} state="open" eventCount={999999} />
|
|
375
406
|
);
|
|
376
407
|
}).not.toThrow();
|
|
377
408
|
});
|
|
@@ -379,7 +410,7 @@ describe('CollaborationPanel Component', () => {
|
|
|
379
410
|
it('should handle negative event counts', () => {
|
|
380
411
|
expect(() => {
|
|
381
412
|
render(
|
|
382
|
-
<CollaborationPanel {...defaultProps}
|
|
413
|
+
<CollaborationPanel {...defaultProps} state="open" eventCount={-5} />
|
|
383
414
|
);
|
|
384
415
|
}).not.toThrow();
|
|
385
416
|
});
|
|
@@ -432,7 +463,7 @@ describe('CollaborationPanel Component', () => {
|
|
|
432
463
|
});
|
|
433
464
|
|
|
434
465
|
it('should have visible status indicators', () => {
|
|
435
|
-
const { container } = render(<CollaborationPanel {...defaultProps}
|
|
466
|
+
const { container } = render(<CollaborationPanel {...defaultProps} state="open" />);
|
|
436
467
|
|
|
437
468
|
// Should have a visible status dot
|
|
438
469
|
const indicator = container.querySelector('.semiont-collaboration-panel__dot');
|
|
@@ -8,7 +8,7 @@ import { CommentEntry } from '../CommentEntry';
|
|
|
8
8
|
import type { components } from '@semiont/core';
|
|
9
9
|
import type { EventBus } from "@semiont/core"
|
|
10
10
|
|
|
11
|
-
type Annotation
|
|
11
|
+
import type { Annotation } from '@semiont/core';
|
|
12
12
|
|
|
13
13
|
// Mock TranslationContext
|
|
14
14
|
vi.mock('../../../../contexts/TranslationContext', () => ({
|
|
@@ -24,8 +24,8 @@ vi.mock('../../../../contexts/TranslationContext', () => ({
|
|
|
24
24
|
}));
|
|
25
25
|
|
|
26
26
|
// Mock @semiont/api-client utilities
|
|
27
|
-
vi.mock('@semiont/
|
|
28
|
-
const actual = await vi.importActual('@semiont/
|
|
27
|
+
vi.mock('@semiont/core', async () => {
|
|
28
|
+
const actual = await vi.importActual('@semiont/core');
|
|
29
29
|
return {
|
|
30
30
|
...actual,
|
|
31
31
|
getCommentText: vi.fn(),
|
|
@@ -33,7 +33,7 @@ vi.mock('@semiont/api-client', async () => {
|
|
|
33
33
|
};
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
-
import { getCommentText, getAnnotationExactText } from '@semiont/
|
|
36
|
+
import { getCommentText, getAnnotationExactText } from '@semiont/core';
|
|
37
37
|
import type { MockedFunction } from 'vitest';
|
|
38
38
|
|
|
39
39
|
const mockGetCommentText = getCommentText as MockedFunction<typeof getCommentText>;
|
|
@@ -5,10 +5,10 @@ import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
|
5
5
|
import userEvent from '@testing-library/user-event';
|
|
6
6
|
import '@testing-library/jest-dom';
|
|
7
7
|
import { CommentsPanel } from '../CommentsPanel';
|
|
8
|
-
import {
|
|
9
|
-
import
|
|
8
|
+
import type { components, EventBus } from '@semiont/core';
|
|
9
|
+
import { createTestSemiontWrapper } from '../../../../test-utils';
|
|
10
10
|
|
|
11
|
-
type Annotation
|
|
11
|
+
import type { Annotation } from '@semiont/core';
|
|
12
12
|
|
|
13
13
|
// Composition-based event tracker
|
|
14
14
|
interface TrackedEvent {
|
|
@@ -18,59 +18,27 @@ interface TrackedEvent {
|
|
|
18
18
|
|
|
19
19
|
function createEventTracker() {
|
|
20
20
|
const events: TrackedEvent[] = [];
|
|
21
|
-
|
|
22
|
-
function EventTrackingWrapper({ children }: { children: React.ReactNode }) {
|
|
23
|
-
const eventBus = useEventBus();
|
|
24
|
-
|
|
25
|
-
React.useEffect(() => {
|
|
26
|
-
const handlers: Array<() => void> = [];
|
|
27
|
-
|
|
28
|
-
const trackEvent = (eventName: string) => (payload: any) => {
|
|
29
|
-
events.push({ event: eventName, payload });
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const panelEvents = ['mark:submit'] as const;
|
|
33
|
-
|
|
34
|
-
panelEvents.forEach(eventName => {
|
|
35
|
-
const handler = trackEvent(eventName);
|
|
36
|
-
const subscription = eventBus.get(eventName).subscribe(handler);
|
|
37
|
-
handlers.push(subscription);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
return () => {
|
|
41
|
-
handlers.forEach(sub => sub.unsubscribe());
|
|
42
|
-
};
|
|
43
|
-
}, [eventBus]);
|
|
44
|
-
|
|
45
|
-
return <>{children}</>;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
21
|
return {
|
|
49
|
-
EventTrackingWrapper,
|
|
50
22
|
events,
|
|
51
|
-
clear: () => {
|
|
52
|
-
|
|
23
|
+
clear: () => { events.length = 0; },
|
|
24
|
+
_attach(eventBus: EventBus) {
|
|
25
|
+
const panelEvents = ['mark:submit'] as const;
|
|
26
|
+
panelEvents.forEach((eventName) => {
|
|
27
|
+
eventBus.get(eventName).subscribe((payload: any) => {
|
|
28
|
+
events.push({ event: eventName, payload });
|
|
29
|
+
});
|
|
30
|
+
});
|
|
53
31
|
},
|
|
54
32
|
};
|
|
55
33
|
}
|
|
56
34
|
|
|
57
|
-
// Helper to render with EventBusProvider
|
|
58
35
|
const renderWithEventBus = (component: React.ReactElement, tracker?: ReturnType<typeof createEventTracker>) => {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
{component}
|
|
64
|
-
</tracker.EventTrackingWrapper>
|
|
65
|
-
</EventBusProvider>
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return render(
|
|
70
|
-
<EventBusProvider>
|
|
71
|
-
{component}
|
|
72
|
-
</EventBusProvider>
|
|
36
|
+
const { SemiontWrapper, eventBus } = createTestSemiontWrapper();
|
|
37
|
+
if (tracker) tracker._attach(eventBus);
|
|
38
|
+
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
39
|
+
<SemiontWrapper>{children}</SemiontWrapper>
|
|
73
40
|
);
|
|
41
|
+
return render(component, { wrapper: Wrapper });
|
|
74
42
|
};
|
|
75
43
|
|
|
76
44
|
// Mock TranslationContext
|
|
@@ -89,8 +57,8 @@ vi.mock('../../../../contexts/TranslationContext', () => ({
|
|
|
89
57
|
}));
|
|
90
58
|
|
|
91
59
|
// Mock @semiont/api-client utilities
|
|
92
|
-
vi.mock('@semiont/
|
|
93
|
-
const actual = await vi.importActual('@semiont/
|
|
60
|
+
vi.mock('@semiont/core', async () => {
|
|
61
|
+
const actual = await vi.importActual('@semiont/core');
|
|
94
62
|
return {
|
|
95
63
|
...actual,
|
|
96
64
|
getTextPositionSelector: vi.fn(),
|
|
@@ -116,8 +84,7 @@ vi.mock('../CommentEntry', () => ({
|
|
|
116
84
|
),
|
|
117
85
|
}));
|
|
118
86
|
|
|
119
|
-
import { getTextPositionSelector, getTargetSelector } from '@semiont/
|
|
120
|
-
|
|
87
|
+
import { getTextPositionSelector, getTargetSelector } from '@semiont/core';
|
|
121
88
|
const mockGetTextPositionSelector = getTextPositionSelector as MockedFunction<typeof getTextPositionSelector>;
|
|
122
89
|
const mockGetTargetSelector = getTargetSelector as MockedFunction<typeof getTargetSelector>;
|
|
123
90
|
|
|
@@ -270,9 +237,7 @@ describe('CommentsPanel Component', () => {
|
|
|
270
237
|
];
|
|
271
238
|
|
|
272
239
|
rerender(
|
|
273
|
-
<
|
|
274
|
-
<CommentsPanel {...defaultProps} annotations={updatedComments} />
|
|
275
|
-
</EventBusProvider>
|
|
240
|
+
<CommentsPanel {...defaultProps} annotations={updatedComments} />
|
|
276
241
|
);
|
|
277
242
|
|
|
278
243
|
const comments = screen.getAllByTestId(/comment-/);
|
|
@@ -600,9 +565,7 @@ describe('CommentsPanel Component', () => {
|
|
|
600
565
|
createMockComment(`${j + 1}`, j * 10, (j + 1) * 10)
|
|
601
566
|
);
|
|
602
567
|
rerender(
|
|
603
|
-
<
|
|
604
|
-
<CommentsPanel {...defaultProps} annotations={comments} />
|
|
605
|
-
</EventBusProvider>
|
|
568
|
+
<CommentsPanel {...defaultProps} annotations={comments} />
|
|
606
569
|
);
|
|
607
570
|
}
|
|
608
571
|
|
|
@@ -617,9 +580,7 @@ describe('CommentsPanel Component', () => {
|
|
|
617
580
|
expect(screen.getAllByTestId(/comment-/)).toHaveLength(3);
|
|
618
581
|
|
|
619
582
|
rerender(
|
|
620
|
-
<
|
|
621
|
-
<CommentsPanel {...defaultProps} annotations={mockComments.single} />
|
|
622
|
-
</EventBusProvider>
|
|
583
|
+
<CommentsPanel {...defaultProps} annotations={mockComments.single} />
|
|
623
584
|
);
|
|
624
585
|
|
|
625
586
|
expect(screen.getAllByTestId(/comment-/)).toHaveLength(1);
|
|
@@ -6,18 +6,18 @@ import { renderWithProviders } from '../../../../test-utils';
|
|
|
6
6
|
import userEvent from '@testing-library/user-event';
|
|
7
7
|
import type { components } from '@semiont/core';
|
|
8
8
|
|
|
9
|
-
type Annotation
|
|
9
|
+
import type { Annotation } from '@semiont/core';
|
|
10
10
|
|
|
11
11
|
// Mock @semiont/api-client
|
|
12
|
-
vi.mock('@semiont/
|
|
13
|
-
const actual = await vi.importActual('@semiont/
|
|
12
|
+
vi.mock('@semiont/core', async () => {
|
|
13
|
+
const actual = await vi.importActual('@semiont/core');
|
|
14
14
|
return {
|
|
15
15
|
...actual,
|
|
16
16
|
getAnnotationExactText: vi.fn(),
|
|
17
17
|
};
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
-
import { getAnnotationExactText } from '@semiont/
|
|
20
|
+
import { getAnnotationExactText } from '@semiont/core';
|
|
21
21
|
import type { MockedFunction } from 'vitest';
|
|
22
22
|
import { HighlightEntry } from '../HighlightEntry';
|
|
23
23
|
|
|
@@ -18,7 +18,7 @@ import { renderWithProviders } from '../../../../test-utils';
|
|
|
18
18
|
import { HighlightPanel } from '../HighlightPanel';
|
|
19
19
|
import type { components } from '@semiont/core';
|
|
20
20
|
|
|
21
|
-
type Annotation
|
|
21
|
+
import type { Annotation } from '@semiont/core';
|
|
22
22
|
|
|
23
23
|
// Mock translations - simulates useTranslations('HighlightPanel')
|
|
24
24
|
// The mock receives keys like 'title', 'noHighlights', etc. and returns translated strings
|
|
@@ -57,7 +57,6 @@ describe('HighlightPanel + AssistSection Integration', () => {
|
|
|
57
57
|
{
|
|
58
58
|
id: 'highlight-1',
|
|
59
59
|
motivation: 'highlighting',
|
|
60
|
-
body: [],
|
|
61
60
|
target: {
|
|
62
61
|
source: 'resource-1',
|
|
63
62
|
selector: {
|
|
@@ -5,10 +5,10 @@ import '@testing-library/jest-dom';
|
|
|
5
5
|
import { renderWithProviders } from '../../../../test-utils';
|
|
6
6
|
import userEvent from '@testing-library/user-event';
|
|
7
7
|
import type { components } from '@semiont/core';
|
|
8
|
-
import {
|
|
8
|
+
import { BindNamespace } from '@semiont/sdk';
|
|
9
9
|
import type { RouteBuilder } from '../../../../contexts/RoutingContext';
|
|
10
10
|
|
|
11
|
-
type Annotation
|
|
11
|
+
import type { Annotation } from '@semiont/core';
|
|
12
12
|
|
|
13
13
|
// Stable mock functions defined outside vi.mock to avoid re-render loops
|
|
14
14
|
const mockGetAnnotationExactText = vi.fn();
|
|
@@ -21,8 +21,8 @@ const mockGetEntityTypes = vi.fn();
|
|
|
21
21
|
const mockNavigate = vi.fn();
|
|
22
22
|
const mockHoverProps = { onMouseEnter: vi.fn(), onMouseLeave: vi.fn() };
|
|
23
23
|
|
|
24
|
-
vi.mock('@semiont/
|
|
25
|
-
const actual = await vi.importActual('@semiont/
|
|
24
|
+
vi.mock('@semiont/core', async () => {
|
|
25
|
+
const actual = await vi.importActual('@semiont/core');
|
|
26
26
|
return {
|
|
27
27
|
...actual,
|
|
28
28
|
getAnnotationExactText: (...args: unknown[]) => mockGetAnnotationExactText(...args),
|
|
@@ -46,7 +46,7 @@ vi.mock('../../../../hooks/useObservableBrowse', () => ({
|
|
|
46
46
|
useObservableExternalNavigation: () => mockNavigate,
|
|
47
47
|
}));
|
|
48
48
|
|
|
49
|
-
vi.mock('../../../../hooks/
|
|
49
|
+
vi.mock('../../../../hooks/useHoverEmitter', () => ({
|
|
50
50
|
useHoverEmitter: () => mockHoverProps,
|
|
51
51
|
}));
|
|
52
52
|
|
|
@@ -308,7 +308,7 @@ describe('ReferenceEntry', () => {
|
|
|
308
308
|
mockIsBodyResolved.mockReturnValue(true);
|
|
309
309
|
mockGetBodySource.mockReturnValue('linked-doc');
|
|
310
310
|
|
|
311
|
-
const bindSpy = vi.spyOn(
|
|
311
|
+
const bindSpy = vi.spyOn(BindNamespace.prototype, 'body').mockResolvedValue(undefined);
|
|
312
312
|
|
|
313
313
|
const { container } = renderWithProviders(
|
|
314
314
|
<ReferenceEntry {...defaultProps} annotateMode={true} />,
|
|
@@ -320,8 +320,7 @@ describe('ReferenceEntry', () => {
|
|
|
320
320
|
expect(bindSpy).toHaveBeenCalledWith(
|
|
321
321
|
'resource-1',
|
|
322
322
|
'ref-1',
|
|
323
|
-
|
|
324
|
-
expect.anything(),
|
|
323
|
+
[{ op: 'remove', item: { type: 'SpecificResource', source: 'linked-doc', purpose: 'linking' } }],
|
|
325
324
|
);
|
|
326
325
|
|
|
327
326
|
bindSpy.mockRestore();
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Triangulation test for the VM → useObservable → prop → chip render chain.
|
|
3
|
+
*
|
|
4
|
+
* Written after an e2e failure (test 05) where `ReferencesPanel` rendered
|
|
5
|
+
* "No entity types available" even though the client provably received a
|
|
6
|
+
* 9-string entity-types array from the backend.
|
|
7
|
+
*
|
|
8
|
+
* This test closes the Layer 5-6 gap: the existing `ResourceViewerPage.test.tsx`
|
|
9
|
+
* stubs `UnifiedAnnotationsPanel`/`ReferencesPanel` as `<div data-testid>`s,
|
|
10
|
+
* so it never verifies that an observable emitting [9 strings] actually
|
|
11
|
+
* produces 9 chips in the DOM. This test does.
|
|
12
|
+
*
|
|
13
|
+
* If this test passes and the e2e still fails, the bug is further upstream
|
|
14
|
+
* (BrowseNamespace wiring, multiple ApiClient instances, SSE delivery).
|
|
15
|
+
* If it fails, the bug is here in component-land.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
19
|
+
import React from 'react';
|
|
20
|
+
import { render, screen, act } from '@testing-library/react';
|
|
21
|
+
import '@testing-library/jest-dom';
|
|
22
|
+
import { BehaviorSubject } from 'rxjs';
|
|
23
|
+
import { ReferencesPanel } from '../ReferencesPanel';
|
|
24
|
+
import { createTestSemiontWrapper } from '../../../../test-utils';
|
|
25
|
+
import { useObservable } from '../../../../hooks/useObservable';
|
|
26
|
+
|
|
27
|
+
// Match ReferencesPanel.test.tsx's i18n mocking so the test doesn't
|
|
28
|
+
// depend on a real translation setup.
|
|
29
|
+
vi.mock('../../../../contexts/TranslationContext', () => ({
|
|
30
|
+
useTranslations: () => (key: string) => {
|
|
31
|
+
const map: Record<string, string> = {
|
|
32
|
+
title: 'References',
|
|
33
|
+
annotateReferences: 'Annotate References',
|
|
34
|
+
entityTypesOptional: 'Entity types',
|
|
35
|
+
noEntityTypes: 'No entity types available',
|
|
36
|
+
selectEntityTypes: 'Select entity types',
|
|
37
|
+
includeDescriptiveReferences: 'Include descriptive references',
|
|
38
|
+
cancel: 'Cancel',
|
|
39
|
+
createReference: 'Create Reference',
|
|
40
|
+
outgoingReferences: 'Outgoing References',
|
|
41
|
+
incomingReferences: 'Incoming References',
|
|
42
|
+
noIncomingReferences: 'No incoming references',
|
|
43
|
+
fragmentSelected: 'Fragment selected',
|
|
44
|
+
annotate: 'Annotate',
|
|
45
|
+
start: 'Start',
|
|
46
|
+
};
|
|
47
|
+
return map[key] ?? key;
|
|
48
|
+
},
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
vi.mock('../AssistSection', () => ({
|
|
52
|
+
AssistSection: () => null,
|
|
53
|
+
AnnotateReferencesProgressWidget: () => null,
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
const NINE_TYPES = [
|
|
57
|
+
'Author', 'Concept', 'Date', 'Event', 'Location',
|
|
58
|
+
'Organization', 'Person', 'Product', 'Technology',
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const MockLink: React.FC<{ href?: string; children?: React.ReactNode }> = ({ children }) => <>{children}</>;
|
|
62
|
+
const mockRoutes = { resourceDetail: (id: string) => `/resource/${id}` } as any;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Thin harness: subscribes to a BehaviorSubject via useObservable (the
|
|
66
|
+
* same hook ResourceViewerPage uses for vm.entityTypes$) and forwards
|
|
67
|
+
* its value into ReferencesPanel as `allEntityTypes`.
|
|
68
|
+
*/
|
|
69
|
+
function ObservableHarness({ source$ }: { source$: BehaviorSubject<string[]> }) {
|
|
70
|
+
const entityTypes = useObservable(source$) ?? [];
|
|
71
|
+
return (
|
|
72
|
+
<ReferencesPanel
|
|
73
|
+
annotations={[]}
|
|
74
|
+
isAssisting={false}
|
|
75
|
+
progress={null}
|
|
76
|
+
annotateMode={true}
|
|
77
|
+
Link={MockLink}
|
|
78
|
+
routes={mockRoutes}
|
|
79
|
+
allEntityTypes={entityTypes}
|
|
80
|
+
pendingAnnotation={{
|
|
81
|
+
motivation: 'linking',
|
|
82
|
+
selector: { type: 'TextQuoteSelector', exact: 'te' },
|
|
83
|
+
} as any}
|
|
84
|
+
/>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const renderWithBus = (ui: React.ReactElement) => {
|
|
89
|
+
const { SemiontWrapper } = createTestSemiontWrapper();
|
|
90
|
+
return render(<SemiontWrapper>{ui}</SemiontWrapper>);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
describe('Layer 5-6 — VM observable → useObservable → ReferencesPanel chips', () => {
|
|
94
|
+
it('an observable seeded with [9 strings] renders 9 pending-reference chips', async () => {
|
|
95
|
+
const source$ = new BehaviorSubject<string[]>(NINE_TYPES);
|
|
96
|
+
renderWithBus(<ObservableHarness source$={source$} />);
|
|
97
|
+
|
|
98
|
+
// Wait for useEffect in useObservable to run and commit the value.
|
|
99
|
+
const chips = await screen.findAllByRole('button', { name: (_, el) =>
|
|
100
|
+
el.classList.contains('semiont-tag-selector__item') ?? false,
|
|
101
|
+
});
|
|
102
|
+
expect(chips).toHaveLength(NINE_TYPES.length);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('transition [] → [9 strings] re-renders with 9 chips', async () => {
|
|
106
|
+
// This is the production timeline: the Cache emits undefined (mapped
|
|
107
|
+
// to []) first, then the 9-string array once fetch resolves. The
|
|
108
|
+
// prop chain must survive this transition.
|
|
109
|
+
const source$ = new BehaviorSubject<string[]>([]);
|
|
110
|
+
renderWithBus(<ObservableHarness source$={source$} />);
|
|
111
|
+
|
|
112
|
+
// Initially: no chips (the gate is allEntityTypes.length > 0).
|
|
113
|
+
expect(document.querySelectorAll('.semiont-tag-selector__item').length).toBe(0);
|
|
114
|
+
|
|
115
|
+
await act(async () => {
|
|
116
|
+
source$.next(NINE_TYPES);
|
|
117
|
+
// Let useObservable's setState flush.
|
|
118
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
expect(document.querySelectorAll('.semiont-tag-selector__item').length).toBe(NINE_TYPES.length);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('[9 strings] with duplicate emissions (simulating SSE-duplicate deliveries) renders 9 chips', async () => {
|
|
125
|
+
// The failing e2e run showed the same correlationId delivered 3x due
|
|
126
|
+
// to concurrent SSE streams. Same data, but multiple BehaviorSubject
|
|
127
|
+
// writes. Must not clobber the render.
|
|
128
|
+
const source$ = new BehaviorSubject<string[]>([]);
|
|
129
|
+
renderWithBus(<ObservableHarness source$={source$} />);
|
|
130
|
+
|
|
131
|
+
await act(async () => {
|
|
132
|
+
source$.next(NINE_TYPES);
|
|
133
|
+
source$.next([...NINE_TYPES]); // same content, new reference
|
|
134
|
+
source$.next([...NINE_TYPES]);
|
|
135
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
expect(document.querySelectorAll('.semiont-tag-selector__item').length).toBe(NINE_TYPES.length);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('no regression: [] still renders "No entity types available"', () => {
|
|
142
|
+
// Confirms the control case: an empty observable does render the
|
|
143
|
+
// message the failing e2e saw. Guards against the test passing
|
|
144
|
+
// trivially because of a selector bug.
|
|
145
|
+
const source$ = new BehaviorSubject<string[]>([]);
|
|
146
|
+
renderWithBus(<ObservableHarness source$={source$} />);
|
|
147
|
+
|
|
148
|
+
// There are two such text nodes in the panel (pending prompt + assist
|
|
149
|
+
// section), but both correspond to the same allEntityTypes=[] state.
|
|
150
|
+
const msg = screen.queryAllByText(/no entity types available/i);
|
|
151
|
+
expect(msg.length).toBeGreaterThan(0);
|
|
152
|
+
});
|
|
153
|
+
});
|