@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
|
@@ -5,8 +5,8 @@ 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
11
|
type Annotation = components['schemas']['Annotation'];
|
|
12
12
|
|
|
@@ -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
|
|
@@ -270,9 +238,7 @@ describe('CommentsPanel Component', () => {
|
|
|
270
238
|
];
|
|
271
239
|
|
|
272
240
|
rerender(
|
|
273
|
-
<
|
|
274
|
-
<CommentsPanel {...defaultProps} annotations={updatedComments} />
|
|
275
|
-
</EventBusProvider>
|
|
241
|
+
<CommentsPanel {...defaultProps} annotations={updatedComments} />
|
|
276
242
|
);
|
|
277
243
|
|
|
278
244
|
const comments = screen.getAllByTestId(/comment-/);
|
|
@@ -600,9 +566,7 @@ describe('CommentsPanel Component', () => {
|
|
|
600
566
|
createMockComment(`${j + 1}`, j * 10, (j + 1) * 10)
|
|
601
567
|
);
|
|
602
568
|
rerender(
|
|
603
|
-
<
|
|
604
|
-
<CommentsPanel {...defaultProps} annotations={comments} />
|
|
605
|
-
</EventBusProvider>
|
|
569
|
+
<CommentsPanel {...defaultProps} annotations={comments} />
|
|
606
570
|
);
|
|
607
571
|
}
|
|
608
572
|
|
|
@@ -617,9 +581,7 @@ describe('CommentsPanel Component', () => {
|
|
|
617
581
|
expect(screen.getAllByTestId(/comment-/)).toHaveLength(3);
|
|
618
582
|
|
|
619
583
|
rerender(
|
|
620
|
-
<
|
|
621
|
-
<CommentsPanel {...defaultProps} annotations={mockComments.single} />
|
|
622
|
-
</EventBusProvider>
|
|
584
|
+
<CommentsPanel {...defaultProps} annotations={mockComments.single} />
|
|
623
585
|
);
|
|
624
586
|
|
|
625
587
|
expect(screen.getAllByTestId(/comment-/)).toHaveLength(1);
|
|
@@ -5,7 +5,7 @@ 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/api-client';
|
|
9
9
|
import type { RouteBuilder } from '../../../../contexts/RoutingContext';
|
|
10
10
|
|
|
11
11
|
type Annotation = components['schemas']['Annotation'];
|
|
@@ -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
|
+
});
|
|
@@ -4,7 +4,8 @@ import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
|
4
4
|
import userEvent from '@testing-library/user-event';
|
|
5
5
|
import '@testing-library/jest-dom';
|
|
6
6
|
import { ReferencesPanel } from '../ReferencesPanel';
|
|
7
|
-
import {
|
|
7
|
+
import type { EventBus } from '@semiont/core';
|
|
8
|
+
import { createTestSemiontWrapper } from '../../../../test-utils';
|
|
8
9
|
|
|
9
10
|
// Composition-based event tracker
|
|
10
11
|
interface TrackedEvent {
|
|
@@ -14,59 +15,27 @@ interface TrackedEvent {
|
|
|
14
15
|
|
|
15
16
|
function createEventTracker() {
|
|
16
17
|
const events: TrackedEvent[] = [];
|
|
17
|
-
|
|
18
|
-
function EventTrackingWrapper({ children }: { children: React.ReactNode }) {
|
|
19
|
-
const eventBus = useEventBus();
|
|
20
|
-
|
|
21
|
-
React.useEffect(() => {
|
|
22
|
-
const handlers: Array<() => void> = [];
|
|
23
|
-
|
|
24
|
-
const trackEvent = (eventName: string) => (payload: any) => {
|
|
25
|
-
events.push({ event: eventName, payload });
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
const panelEvents = ['mark:assist-request'] as const;
|
|
29
|
-
|
|
30
|
-
panelEvents.forEach(eventName => {
|
|
31
|
-
const handler = trackEvent(eventName);
|
|
32
|
-
const subscription = eventBus.get(eventName).subscribe(handler);
|
|
33
|
-
handlers.push(subscription);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
return () => {
|
|
37
|
-
handlers.forEach(sub => sub.unsubscribe());
|
|
38
|
-
};
|
|
39
|
-
}, [eventBus]);
|
|
40
|
-
|
|
41
|
-
return <>{children}</>;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
18
|
return {
|
|
45
|
-
EventTrackingWrapper,
|
|
46
19
|
events,
|
|
47
|
-
clear: () => {
|
|
48
|
-
|
|
20
|
+
clear: () => { events.length = 0; },
|
|
21
|
+
_attach(eventBus: EventBus) {
|
|
22
|
+
const panelEvents = ['mark:assist-request'] as const;
|
|
23
|
+
panelEvents.forEach((eventName) => {
|
|
24
|
+
eventBus.get(eventName).subscribe((payload: any) => {
|
|
25
|
+
events.push({ event: eventName, payload });
|
|
26
|
+
});
|
|
27
|
+
});
|
|
49
28
|
},
|
|
50
29
|
};
|
|
51
30
|
}
|
|
52
31
|
|
|
53
|
-
// Helper to render with EventBusProvider
|
|
54
32
|
const renderWithEventBus = (component: React.ReactElement, tracker?: ReturnType<typeof createEventTracker>) => {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
{component}
|
|
60
|
-
</tracker.EventTrackingWrapper>
|
|
61
|
-
</EventBusProvider>
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return render(
|
|
66
|
-
<EventBusProvider>
|
|
67
|
-
{component}
|
|
68
|
-
</EventBusProvider>
|
|
33
|
+
const { SemiontWrapper, eventBus } = createTestSemiontWrapper();
|
|
34
|
+
if (tracker) tracker._attach(eventBus);
|
|
35
|
+
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
36
|
+
<SemiontWrapper>{children}</SemiontWrapper>
|
|
69
37
|
);
|
|
38
|
+
return render(component, { wrapper: Wrapper });
|
|
70
39
|
};
|
|
71
40
|
|
|
72
41
|
// Mock TranslationContext
|
|
@@ -344,26 +313,22 @@ describe('ReferencesPanel Component', () => {
|
|
|
344
313
|
|
|
345
314
|
// Simulate detection starting
|
|
346
315
|
rerender(
|
|
347
|
-
<
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
/>
|
|
353
|
-
</EventBusProvider>
|
|
316
|
+
<ReferencesPanel
|
|
317
|
+
{...defaultProps}
|
|
318
|
+
isAssisting={true}
|
|
319
|
+
progress={{ completedEntityTypes: [] }}
|
|
320
|
+
/>
|
|
354
321
|
);
|
|
355
322
|
|
|
356
323
|
// Simulate detection completing
|
|
357
324
|
rerender(
|
|
358
|
-
<
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
/>
|
|
366
|
-
</EventBusProvider>
|
|
325
|
+
<ReferencesPanel
|
|
326
|
+
{...defaultProps}
|
|
327
|
+
isAssisting={false}
|
|
328
|
+
progress={{
|
|
329
|
+
completedEntityTypes: [{ entityType: 'Person', foundCount: 5 }],
|
|
330
|
+
}}
|
|
331
|
+
/>
|
|
367
332
|
);
|
|
368
333
|
|
|
369
334
|
// UI should reset but we can't directly test internal state
|
|
@@ -473,13 +438,11 @@ describe('ReferencesPanel Component', () => {
|
|
|
473
438
|
|
|
474
439
|
// Parent clears progress after completion
|
|
475
440
|
rerender(
|
|
476
|
-
<
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
/>
|
|
482
|
-
</EventBusProvider>
|
|
441
|
+
<ReferencesPanel
|
|
442
|
+
{...defaultProps}
|
|
443
|
+
isAssisting={false}
|
|
444
|
+
progress={null}
|
|
445
|
+
/>
|
|
483
446
|
);
|
|
484
447
|
|
|
485
448
|
expect(screen.getByText('Person:')).toBeInTheDocument();
|
|
@@ -498,9 +461,7 @@ describe('ReferencesPanel Component', () => {
|
|
|
498
461
|
);
|
|
499
462
|
|
|
500
463
|
rerender(
|
|
501
|
-
<
|
|
502
|
-
<ReferencesPanel {...defaultProps} isAssisting={false} progress={null} />
|
|
503
|
-
</EventBusProvider>
|
|
464
|
+
<ReferencesPanel {...defaultProps} isAssisting={false} progress={null} />
|
|
504
465
|
);
|
|
505
466
|
expect(screen.getByText(/Found.*5/i)).toBeInTheDocument();
|
|
506
467
|
});
|
|
@@ -517,9 +478,7 @@ describe('ReferencesPanel Component', () => {
|
|
|
517
478
|
);
|
|
518
479
|
|
|
519
480
|
rerender(
|
|
520
|
-
<
|
|
521
|
-
<ReferencesPanel {...defaultProps} isAssisting={false} progress={null} />
|
|
522
|
-
</EventBusProvider>
|
|
481
|
+
<ReferencesPanel {...defaultProps} isAssisting={false} progress={null} />
|
|
523
482
|
);
|
|
524
483
|
expect(screen.getByText('✓')).toBeInTheDocument();
|
|
525
484
|
});
|
|
@@ -536,9 +495,7 @@ describe('ReferencesPanel Component', () => {
|
|
|
536
495
|
);
|
|
537
496
|
|
|
538
497
|
rerender(
|
|
539
|
-
<
|
|
540
|
-
<ReferencesPanel {...defaultProps} isAssisting={false} progress={null} />
|
|
541
|
-
</EventBusProvider>
|
|
498
|
+
<ReferencesPanel {...defaultProps} isAssisting={false} progress={null} />
|
|
542
499
|
);
|
|
543
500
|
|
|
544
501
|
// Should show both the completed log AND the selection UI
|
|
@@ -558,9 +515,7 @@ describe('ReferencesPanel Component', () => {
|
|
|
558
515
|
);
|
|
559
516
|
|
|
560
517
|
rerender(
|
|
561
|
-
<
|
|
562
|
-
<ReferencesPanel {...defaultProps} isAssisting={false} progress={null} />
|
|
563
|
-
</EventBusProvider>
|
|
518
|
+
<ReferencesPanel {...defaultProps} isAssisting={false} progress={null} />
|
|
564
519
|
);
|
|
565
520
|
|
|
566
521
|
// Selection UI should be immediately available (no button click needed)
|
|
@@ -594,13 +549,11 @@ describe('ReferencesPanel Component', () => {
|
|
|
594
549
|
|
|
595
550
|
// Start detecting
|
|
596
551
|
rerender(
|
|
597
|
-
<
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
/>
|
|
603
|
-
</EventBusProvider>
|
|
552
|
+
<ReferencesPanel
|
|
553
|
+
{...defaultProps}
|
|
554
|
+
isAssisting={true}
|
|
555
|
+
progress={{ completedEntityTypes: [] }}
|
|
556
|
+
/>
|
|
604
557
|
);
|
|
605
558
|
|
|
606
559
|
// Detecting state
|
|
@@ -622,22 +575,18 @@ describe('ReferencesPanel Component', () => {
|
|
|
622
575
|
|
|
623
576
|
// Complete - first trigger useEffect to copy to lastDetectionLog
|
|
624
577
|
rerender(
|
|
625
|
-
<
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
/>
|
|
633
|
-
</EventBusProvider>
|
|
578
|
+
<ReferencesPanel
|
|
579
|
+
{...defaultProps}
|
|
580
|
+
isAssisting={false}
|
|
581
|
+
progress={{
|
|
582
|
+
completedEntityTypes: [{ entityType: 'Person', foundCount: 5 }],
|
|
583
|
+
}}
|
|
584
|
+
/>
|
|
634
585
|
);
|
|
635
586
|
|
|
636
587
|
// Then clear progress to show the log
|
|
637
588
|
rerender(
|
|
638
|
-
<
|
|
639
|
-
<ReferencesPanel {...defaultProps} isAssisting={false} progress={null} />
|
|
640
|
-
</EventBusProvider>
|
|
589
|
+
<ReferencesPanel {...defaultProps} isAssisting={false} progress={null} />
|
|
641
590
|
);
|
|
642
591
|
|
|
643
592
|
expect(screen.queryByTestId('annotation-progress-widget')).not.toBeInTheDocument();
|
|
@@ -659,18 +608,14 @@ describe('ReferencesPanel Component', () => {
|
|
|
659
608
|
|
|
660
609
|
// Clear progress to show the log
|
|
661
610
|
rerender(
|
|
662
|
-
<
|
|
663
|
-
<ReferencesPanel {...defaultProps} isAssisting={false} progress={null} />
|
|
664
|
-
</EventBusProvider>
|
|
611
|
+
<ReferencesPanel {...defaultProps} isAssisting={false} progress={null} />
|
|
665
612
|
);
|
|
666
613
|
|
|
667
614
|
// Selection UI should be immediately available
|
|
668
615
|
expect(screen.getByText('Select entity types')).toBeInTheDocument();
|
|
669
616
|
|
|
670
617
|
rerender(
|
|
671
|
-
<
|
|
672
|
-
<ReferencesPanel {...defaultProps} />
|
|
673
|
-
</EventBusProvider>
|
|
618
|
+
<ReferencesPanel {...defaultProps} />
|
|
674
619
|
);
|
|
675
620
|
|
|
676
621
|
expect(screen.getByText('Select entity types')).toBeInTheDocument();
|
|
@@ -2,8 +2,9 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
4
4
|
import '@testing-library/jest-dom';
|
|
5
|
+
import type { EventBus } from '@semiont/core';
|
|
5
6
|
import { ResourceInfoPanel } from '../ResourceInfoPanel';
|
|
6
|
-
import {
|
|
7
|
+
import { createTestSemiontWrapper } from '../../../../test-utils';
|
|
7
8
|
|
|
8
9
|
// Mock TranslationContext
|
|
9
10
|
vi.mock('../../../../contexts/TranslationContext', () => ({
|
|
@@ -61,64 +62,31 @@ interface TrackedEvent {
|
|
|
61
62
|
|
|
62
63
|
function createEventTracker() {
|
|
63
64
|
const events: TrackedEvent[] = [];
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
React.useEffect(() => {
|
|
69
|
-
const handlers: Array<() => void> = [];
|
|
70
|
-
|
|
71
|
-
// Track resource-related events
|
|
72
|
-
const trackEvent = (eventName: string) => (payload: any) => {
|
|
73
|
-
events.push({ event: eventName, payload });
|
|
74
|
-
};
|
|
75
|
-
|
|
65
|
+
return {
|
|
66
|
+
events,
|
|
67
|
+
clear: () => { events.length = 0; },
|
|
68
|
+
_attach(eventBus: EventBus) {
|
|
76
69
|
const resourceEvents = [
|
|
77
70
|
'yield:clone',
|
|
78
71
|
'mark:archive',
|
|
79
72
|
'mark:unarchive',
|
|
80
73
|
] as const;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
handlers.push(subscription);
|
|
74
|
+
resourceEvents.forEach((eventName) => {
|
|
75
|
+
eventBus.get(eventName).subscribe((payload: any) => {
|
|
76
|
+
events.push({ event: eventName, payload });
|
|
77
|
+
});
|
|
86
78
|
});
|
|
87
|
-
|
|
88
|
-
return () => {
|
|
89
|
-
handlers.forEach(sub => sub.unsubscribe());
|
|
90
|
-
};
|
|
91
|
-
}, [eventBus]);
|
|
92
|
-
|
|
93
|
-
return <>{children}</>;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return {
|
|
97
|
-
EventTrackingWrapper,
|
|
98
|
-
events,
|
|
99
|
-
clear: () => {
|
|
100
|
-
events.length = 0;
|
|
101
79
|
},
|
|
102
80
|
};
|
|
103
81
|
}
|
|
104
82
|
|
|
105
|
-
// Helper to render with EventBusProvider
|
|
106
83
|
const renderWithEventBus = (component: React.ReactElement, tracker?: ReturnType<typeof createEventTracker>) => {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
{component}
|
|
112
|
-
</tracker.EventTrackingWrapper>
|
|
113
|
-
</EventBusProvider>
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return render(
|
|
118
|
-
<EventBusProvider>
|
|
119
|
-
{component}
|
|
120
|
-
</EventBusProvider>
|
|
84
|
+
const { SemiontWrapper, eventBus } = createTestSemiontWrapper();
|
|
85
|
+
if (tracker) tracker._attach(eventBus);
|
|
86
|
+
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
87
|
+
<SemiontWrapper>{children}</SemiontWrapper>
|
|
121
88
|
);
|
|
89
|
+
return render(component, { wrapper: Wrapper });
|
|
122
90
|
};
|
|
123
91
|
|
|
124
92
|
describe('ResourceInfoPanel Component', () => {
|