@semiont/react-ui 0.2.33-build.78 → 0.2.33-build.79
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{ar-RNNSPLQB.mjs → ar-EMHEHPCJ.mjs} +2 -1
- package/dist/ar-EMHEHPCJ.mjs.map +1 -0
- package/dist/{bn-S2CDL7EC.mjs → bn-OVCI4F6X.mjs} +2 -1
- package/dist/bn-OVCI4F6X.mjs.map +1 -0
- package/dist/{chunk-35LLVRFK.mjs → chunk-JZIO2A3B.mjs} +31 -31
- package/dist/{chunk-UDX2Q35T.mjs → chunk-LIHZTECW.mjs} +2 -1
- package/dist/chunk-LIHZTECW.mjs.map +1 -0
- package/dist/{cs-RSV675WU.mjs → cs-FAN66Q2F.mjs} +2 -1
- package/dist/cs-FAN66Q2F.mjs.map +1 -0
- package/dist/{da-CHXNPWJC.mjs → da-YBBIHI2O.mjs} +2 -1
- package/dist/da-YBBIHI2O.mjs.map +1 -0
- package/dist/{de-KPEZ53D4.mjs → de-MAYU33LB.mjs} +2 -1
- package/dist/de-MAYU33LB.mjs.map +1 -0
- package/dist/{el-MW2BME5T.mjs → el-MKGSWN4O.mjs} +2 -1
- package/dist/el-MKGSWN4O.mjs.map +1 -0
- package/dist/{en-EVMIX24Y.mjs → en-DDLIXJCU.mjs} +2 -2
- package/dist/{es-HQ24NYS3.mjs → es-52LHUWJD.mjs} +2 -1
- package/dist/es-52LHUWJD.mjs.map +1 -0
- package/dist/{fa-W34LRLHG.mjs → fa-FJICRANB.mjs} +2 -1
- package/dist/fa-FJICRANB.mjs.map +1 -0
- package/dist/{fi-3U44IGOA.mjs → fi-O455XFCR.mjs} +2 -1
- package/dist/fi-O455XFCR.mjs.map +1 -0
- package/dist/{fr-N7DKX6NN.mjs → fr-TXIXHOOE.mjs} +2 -1
- package/dist/fr-TXIXHOOE.mjs.map +1 -0
- package/dist/{he-CS4WRXN3.mjs → he-JBSOX5IN.mjs} +2 -1
- package/dist/he-JBSOX5IN.mjs.map +1 -0
- package/dist/{hi-GJDY46KA.mjs → hi-KGHI3XVT.mjs} +2 -1
- package/dist/hi-KGHI3XVT.mjs.map +1 -0
- package/dist/{id-WAEZJK2Y.mjs → id-5OCPPZLO.mjs} +2 -1
- package/dist/id-5OCPPZLO.mjs.map +1 -0
- package/dist/index.d.mts +102 -106
- package/dist/index.mjs +1814 -1450
- package/dist/index.mjs.map +1 -1
- package/dist/{it-VDNDMZPU.mjs → it-PNBBZSM2.mjs} +2 -1
- package/dist/it-PNBBZSM2.mjs.map +1 -0
- package/dist/{ja-5PEH56J5.mjs → ja-LDD7R3TJ.mjs} +2 -1
- package/dist/ja-LDD7R3TJ.mjs.map +1 -0
- package/dist/{ko-JYPL3WVA.mjs → ko-F47ZDEY3.mjs} +2 -1
- package/dist/ko-F47ZDEY3.mjs.map +1 -0
- package/dist/{ms-5PZVW76T.mjs → ms-Z7LMXJWL.mjs} +2 -1
- package/dist/ms-Z7LMXJWL.mjs.map +1 -0
- package/dist/{nl-YXES36KM.mjs → nl-6SJFBPJ3.mjs} +2 -1
- package/dist/nl-6SJFBPJ3.mjs.map +1 -0
- package/dist/{no-XRA2UCQD.mjs → no-YXPBPSGF.mjs} +2 -1
- package/dist/no-YXPBPSGF.mjs.map +1 -0
- package/dist/{pl-WH6LJA5G.mjs → pl-P4AZ2QME.mjs} +2 -1
- package/dist/pl-P4AZ2QME.mjs.map +1 -0
- package/dist/{pt-7GAG57BM.mjs → pt-LHWUS6U6.mjs} +2 -1
- package/dist/pt-LHWUS6U6.mjs.map +1 -0
- package/dist/{ro-BTDDRB7N.mjs → ro-EA5J2ZON.mjs} +2 -1
- package/dist/ro-EA5J2ZON.mjs.map +1 -0
- package/dist/{sv-7V5C2IT4.mjs → sv-DATBS3UQ.mjs} +2 -1
- package/dist/sv-DATBS3UQ.mjs.map +1 -0
- package/dist/test-utils.mjs +2 -2
- package/dist/{th-LPKYLBX5.mjs → th-WTFJRWPT.mjs} +2 -1
- package/dist/th-WTFJRWPT.mjs.map +1 -0
- package/dist/{tr-DU4RQL4M.mjs → tr-IKO3RXOX.mjs} +2 -1
- package/dist/tr-IKO3RXOX.mjs.map +1 -0
- package/dist/{uk-36UHTDDI.mjs → uk-CF6CTTRK.mjs} +2 -1
- package/dist/uk-CF6CTTRK.mjs.map +1 -0
- package/dist/{vi-GDHOUZDH.mjs → vi-AJLTXPZQ.mjs} +2 -1
- package/dist/vi-AJLTXPZQ.mjs.map +1 -0
- package/dist/{zh-TYUID4XZ.mjs → zh-U3ORHHYH.mjs} +2 -1
- package/dist/zh-U3ORHHYH.mjs.map +1 -0
- package/package.json +6 -2
- package/src/components/resource/AnnotateView.tsx +0 -4
- package/src/components/resource/AnnotationHistory.tsx +12 -13
- package/src/components/resource/BrowseView.tsx +8 -16
- package/src/components/resource/HistoryEvent.tsx +3 -4
- package/src/components/resource/ResourceViewer.tsx +174 -201
- package/src/components/resource/event-formatting.ts +316 -0
- package/src/components/resource/panels/AssessmentPanel.tsx +37 -9
- package/src/components/resource/panels/CollaborationPanel.tsx +20 -13
- package/src/components/resource/panels/CommentsPanel.tsx +38 -9
- package/src/components/resource/panels/ReferencesPanel.tsx +39 -14
- package/src/components/resource/panels/StatisticsPanel.tsx +9 -19
- package/src/components/resource/panels/TaggingPanel.tsx +27 -0
- package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +28 -21
- package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +547 -0
- package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +10 -0
- package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +10 -0
- package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +564 -0
- package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +8 -15
- package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +13 -6
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +147 -78
- package/src/styles/motivations/motivation-assessment.css +28 -0
- package/src/styles/patterns/panel-helpers.css +26 -0
- package/translations/ar.json +1 -0
- package/translations/bn.json +1 -0
- package/translations/cs.json +1 -0
- package/translations/da.json +1 -0
- package/translations/de.json +1 -0
- package/translations/el.json +1 -0
- package/translations/en.json +1 -0
- package/translations/es.json +1 -0
- package/translations/fa.json +1 -0
- package/translations/fi.json +1 -0
- package/translations/fr.json +1 -0
- package/translations/he.json +1 -0
- package/translations/hi.json +1 -0
- package/translations/id.json +1 -0
- package/translations/it.json +1 -0
- package/translations/ja.json +1 -0
- package/translations/ko.json +1 -0
- package/translations/ms.json +1 -0
- package/translations/nl.json +1 -0
- package/translations/no.json +1 -0
- package/translations/pl.json +1 -0
- package/translations/pt.json +1 -0
- package/translations/ro.json +1 -0
- package/translations/sv.json +1 -0
- package/translations/th.json +1 -0
- package/translations/tr.json +1 -0
- package/translations/uk.json +1 -0
- package/translations/vi.json +1 -0
- package/translations/zh.json +1 -0
- package/dist/ar-RNNSPLQB.mjs.map +0 -1
- package/dist/bn-S2CDL7EC.mjs.map +0 -1
- package/dist/chunk-UDX2Q35T.mjs.map +0 -1
- package/dist/cs-RSV675WU.mjs.map +0 -1
- package/dist/da-CHXNPWJC.mjs.map +0 -1
- package/dist/de-KPEZ53D4.mjs.map +0 -1
- package/dist/el-MW2BME5T.mjs.map +0 -1
- package/dist/es-HQ24NYS3.mjs.map +0 -1
- package/dist/fa-W34LRLHG.mjs.map +0 -1
- package/dist/fi-3U44IGOA.mjs.map +0 -1
- package/dist/fr-N7DKX6NN.mjs.map +0 -1
- package/dist/he-CS4WRXN3.mjs.map +0 -1
- package/dist/hi-GJDY46KA.mjs.map +0 -1
- package/dist/id-WAEZJK2Y.mjs.map +0 -1
- package/dist/it-VDNDMZPU.mjs.map +0 -1
- package/dist/ja-5PEH56J5.mjs.map +0 -1
- package/dist/ko-JYPL3WVA.mjs.map +0 -1
- package/dist/ms-5PZVW76T.mjs.map +0 -1
- package/dist/nl-YXES36KM.mjs.map +0 -1
- package/dist/no-XRA2UCQD.mjs.map +0 -1
- package/dist/pl-WH6LJA5G.mjs.map +0 -1
- package/dist/pt-7GAG57BM.mjs.map +0 -1
- package/dist/ro-BTDDRB7N.mjs.map +0 -1
- package/dist/sv-7V5C2IT4.mjs.map +0 -1
- package/dist/th-LPKYLBX5.mjs.map +0 -1
- package/dist/tr-DU4RQL4M.mjs.map +0 -1
- package/dist/uk-36UHTDDI.mjs.map +0 -1
- package/dist/vi-GDHOUZDH.mjs.map +0 -1
- package/dist/zh-TYUID4XZ.mjs.map +0 -1
- /package/dist/{chunk-35LLVRFK.mjs.map → chunk-JZIO2A3B.mjs.map} +0 -0
- /package/dist/{en-EVMIX24Y.mjs.map → en-DDLIXJCU.mjs.map} +0 -0
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect } from 'react';
|
|
4
4
|
import { useTranslations } from '../../../contexts/TranslationContext';
|
|
5
|
+
import { useMakeMeaningEvents } from '../../../contexts/MakeMeaningEventBusContext';
|
|
5
6
|
import type { components, Selector } from '@semiont/api-client';
|
|
6
7
|
import { TagEntry } from './TagEntry';
|
|
7
8
|
import { useAnnotationPanel } from '../../../hooks/useAnnotationPanel';
|
|
@@ -71,6 +72,7 @@ export function TaggingPanel({
|
|
|
71
72
|
pendingAnnotation
|
|
72
73
|
}: TaggingPanelProps) {
|
|
73
74
|
const t = useTranslations('TaggingPanel');
|
|
75
|
+
const eventBus = useMakeMeaningEvents();
|
|
74
76
|
const [selectedSchemaId, setSelectedSchemaId] = useState<string>('legal-irac');
|
|
75
77
|
const [selectedCategories, setSelectedCategories] = useState<Set<string>>(new Set());
|
|
76
78
|
|
|
@@ -125,6 +127,20 @@ export function TaggingPanel({
|
|
|
125
127
|
}
|
|
126
128
|
};
|
|
127
129
|
|
|
130
|
+
// Escape key handler for cancelling pending annotation
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
if (!pendingAnnotation) return;
|
|
133
|
+
|
|
134
|
+
const handleEscape = (e: KeyboardEvent) => {
|
|
135
|
+
if (e.key === 'Escape') {
|
|
136
|
+
eventBus.emit('ui:annotation:cancel-pending');
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
document.addEventListener('keydown', handleEscape);
|
|
141
|
+
return () => document.removeEventListener('keydown', handleEscape);
|
|
142
|
+
}, [pendingAnnotation, eventBus]);
|
|
143
|
+
|
|
128
144
|
// Color schemes are now handled via CSS data attributes
|
|
129
145
|
|
|
130
146
|
return (
|
|
@@ -191,6 +207,17 @@ export function TaggingPanel({
|
|
|
191
207
|
</select>
|
|
192
208
|
</div>
|
|
193
209
|
)}
|
|
210
|
+
|
|
211
|
+
{/* Cancel button */}
|
|
212
|
+
<div className="semiont-annotation-prompt__footer">
|
|
213
|
+
<button
|
|
214
|
+
onClick={() => eventBus.emit('ui:annotation:cancel-pending')}
|
|
215
|
+
className="semiont-button semiont-button--secondary"
|
|
216
|
+
data-type="tag"
|
|
217
|
+
>
|
|
218
|
+
{t('cancel')}
|
|
219
|
+
</button>
|
|
220
|
+
</div>
|
|
194
221
|
</div>
|
|
195
222
|
)}
|
|
196
223
|
|
|
@@ -6,6 +6,7 @@ import type { components, Selector } from '@semiont/api-client';
|
|
|
6
6
|
import type { RouteBuilder, LinkComponentProps } from '../../../contexts/RoutingContext';
|
|
7
7
|
import type { Annotator } from '../../../lib/annotation-registry';
|
|
8
8
|
import { createDetectionHandler } from '../../../lib/annotation-registry';
|
|
9
|
+
import { supportsDetection } from '../../../lib/resource-utils';
|
|
9
10
|
import { StatisticsPanel } from './StatisticsPanel';
|
|
10
11
|
import { HighlightPanel } from './HighlightPanel';
|
|
11
12
|
import { ReferencesPanel } from './ReferencesPanel';
|
|
@@ -108,27 +109,25 @@ export function UnifiedAnnotationsPanel(props: UnifiedAnnotationsPanelProps) {
|
|
|
108
109
|
const t = useTranslations('UnifiedAnnotationsPanel');
|
|
109
110
|
|
|
110
111
|
// Group annotations by type using annotators
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if (annotator) {
|
|
123
|
-
|
|
124
|
-
groups[annotator.internalType] = [];
|
|
125
|
-
}
|
|
126
|
-
groups[annotator.internalType].push(ann);
|
|
112
|
+
const groups: Record<string, Annotation[]> = {
|
|
113
|
+
highlight: [],
|
|
114
|
+
comment: [],
|
|
115
|
+
assessment: [],
|
|
116
|
+
reference: [],
|
|
117
|
+
tag: []
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
for (const ann of props.annotations) {
|
|
121
|
+
const annotator = Object.values(props.annotators).find(a => a.matchesAnnotation(ann));
|
|
122
|
+
if (annotator) {
|
|
123
|
+
if (!groups[annotator.internalType]) {
|
|
124
|
+
groups[annotator.internalType] = [];
|
|
127
125
|
}
|
|
126
|
+
groups[annotator.internalType].push(ann);
|
|
128
127
|
}
|
|
128
|
+
}
|
|
129
129
|
|
|
130
|
-
|
|
131
|
-
}, [props.annotations, props.annotators]);
|
|
130
|
+
const grouped = groups;
|
|
132
131
|
|
|
133
132
|
// Load tab from localStorage (per-resource)
|
|
134
133
|
const [activeTab, setActiveTab] = useState<TabKey>(() => {
|
|
@@ -270,8 +269,17 @@ export function UnifiedAnnotationsPanel(props: UnifiedAnnotationsPanelProps) {
|
|
|
270
269
|
const detectionProgress = isDetecting ? props.detectionProgress : null;
|
|
271
270
|
|
|
272
271
|
// Common props for all annotation panels
|
|
273
|
-
// Create detection handler on-demand if
|
|
274
|
-
|
|
272
|
+
// Create detection handler on-demand if:
|
|
273
|
+
// 1. Annotator supports detection (has detection config)
|
|
274
|
+
// 2. Detection context is provided (API client, state handlers)
|
|
275
|
+
// 3. API client is available (not undefined/null)
|
|
276
|
+
// 4. Resource supports detection (is a text/* media type)
|
|
277
|
+
const onDetect = (
|
|
278
|
+
annotator.detection &&
|
|
279
|
+
props.detectionContext &&
|
|
280
|
+
props.detectionContext.client &&
|
|
281
|
+
supportsDetection(props.mediaType)
|
|
282
|
+
)
|
|
275
283
|
? createDetectionHandler(annotator, props.detectionContext)
|
|
276
284
|
: undefined;
|
|
277
285
|
|
|
@@ -323,7 +331,6 @@ export function UnifiedAnnotationsPanel(props: UnifiedAnnotationsPanelProps) {
|
|
|
323
331
|
onCreateDocument={props.onCreateDocument}
|
|
324
332
|
onSearchDocuments={props.onSearchDocuments}
|
|
325
333
|
generatingReferenceId={props.generatingReferenceId}
|
|
326
|
-
mediaType={props.mediaType}
|
|
327
334
|
referencedBy={props.referencedBy}
|
|
328
335
|
referencedByLoading={props.referencedByLoading}
|
|
329
336
|
Link={props.Link}
|
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import type { MockedFunction } from 'vitest';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
5
|
+
import userEvent from '@testing-library/user-event';
|
|
6
|
+
import '@testing-library/jest-dom';
|
|
7
|
+
import { AssessmentPanel } from '../AssessmentPanel';
|
|
8
|
+
import type { components } from '@semiont/api-client';
|
|
9
|
+
|
|
10
|
+
type Annotation = components['schemas']['Annotation'];
|
|
11
|
+
|
|
12
|
+
// Mock MakeMeaningEventBusContext
|
|
13
|
+
vi.mock('../../../../contexts/MakeMeaningEventBusContext', () => ({
|
|
14
|
+
useMakeMeaningEvents: vi.fn(() => ({
|
|
15
|
+
emit: vi.fn(),
|
|
16
|
+
on: vi.fn(),
|
|
17
|
+
off: vi.fn(),
|
|
18
|
+
})),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
// Mock TranslationContext
|
|
22
|
+
vi.mock('../../../../contexts/TranslationContext', () => ({
|
|
23
|
+
useTranslations: vi.fn(() => (key: string) => {
|
|
24
|
+
const translations: Record<string, string> = {
|
|
25
|
+
title: 'Assessments',
|
|
26
|
+
noAssessments: 'No assessments yet. Select text to add an assessment.',
|
|
27
|
+
assessmentPlaceholder: 'Type your assessment here...',
|
|
28
|
+
save: 'Save',
|
|
29
|
+
cancel: 'Cancel',
|
|
30
|
+
fragmentSelected: 'Fragment selected',
|
|
31
|
+
};
|
|
32
|
+
return translations[key] || key;
|
|
33
|
+
}),
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
// Mock @semiont/api-client utilities
|
|
37
|
+
vi.mock('@semiont/api-client', async () => {
|
|
38
|
+
const actual = await vi.importActual('@semiont/api-client');
|
|
39
|
+
return {
|
|
40
|
+
...actual,
|
|
41
|
+
getTextPositionSelector: vi.fn(),
|
|
42
|
+
getTargetSelector: vi.fn(),
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Mock AssessmentEntry component to simplify testing
|
|
47
|
+
vi.mock('../AssessmentEntry', () => ({
|
|
48
|
+
AssessmentEntry: ({ assessment, onClick, onAssessmentRef, onAssessmentHover }: any) => (
|
|
49
|
+
<div
|
|
50
|
+
data-testid={`assessment-${assessment.id}`}
|
|
51
|
+
onClick={() => onClick()}
|
|
52
|
+
>
|
|
53
|
+
<button
|
|
54
|
+
onMouseEnter={() => onAssessmentHover?.(assessment.id)}
|
|
55
|
+
onMouseLeave={() => onAssessmentHover?.(null)}
|
|
56
|
+
>
|
|
57
|
+
Hover
|
|
58
|
+
</button>
|
|
59
|
+
<div>{assessment.id}</div>
|
|
60
|
+
</div>
|
|
61
|
+
),
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
// Mock DetectSection component
|
|
65
|
+
vi.mock('../DetectSection', () => ({
|
|
66
|
+
DetectSection: ({ annotationType, isDetecting, onDetect }: any) => (
|
|
67
|
+
<div data-testid="detect-section">
|
|
68
|
+
<button onClick={() => onDetect?.('test instructions')}>
|
|
69
|
+
Start Detection
|
|
70
|
+
</button>
|
|
71
|
+
{isDetecting && <div>Detecting...</div>}
|
|
72
|
+
</div>
|
|
73
|
+
),
|
|
74
|
+
}));
|
|
75
|
+
|
|
76
|
+
import { getTextPositionSelector, getTargetSelector } from '@semiont/api-client';
|
|
77
|
+
|
|
78
|
+
const mockGetTextPositionSelector = getTextPositionSelector as MockedFunction<typeof getTextPositionSelector>;
|
|
79
|
+
const mockGetTargetSelector = getTargetSelector as MockedFunction<typeof getTargetSelector>;
|
|
80
|
+
|
|
81
|
+
// Test data fixtures
|
|
82
|
+
const createMockAssessment = (id: string, start: number, end: number): Annotation => ({
|
|
83
|
+
'@context': 'http://www.w3.org/ns/anno.jsonld',
|
|
84
|
+
id,
|
|
85
|
+
type: 'Annotation',
|
|
86
|
+
motivation: 'assessing',
|
|
87
|
+
creator: {
|
|
88
|
+
name: `user${id}@example.com`,
|
|
89
|
+
},
|
|
90
|
+
created: `2024-01-0${id.slice(-1)}T10:00:00Z`,
|
|
91
|
+
modified: `2024-01-0${id.slice(-1)}T10:00:00Z`,
|
|
92
|
+
target: {
|
|
93
|
+
source: 'resource-1',
|
|
94
|
+
selector: {
|
|
95
|
+
type: 'TextPositionSelector',
|
|
96
|
+
start,
|
|
97
|
+
end,
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
body: [
|
|
101
|
+
{
|
|
102
|
+
type: 'TextualBody',
|
|
103
|
+
value: `Assessment ${id}`,
|
|
104
|
+
purpose: 'assessing',
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const mockAssessments = {
|
|
110
|
+
empty: [],
|
|
111
|
+
single: [createMockAssessment('1', 0, 10)],
|
|
112
|
+
multiple: [
|
|
113
|
+
createMockAssessment('1', 50, 60), // Middle position
|
|
114
|
+
createMockAssessment('2', 0, 10), // First position
|
|
115
|
+
createMockAssessment('3', 100, 110), // Last position
|
|
116
|
+
],
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// Helper to create pending annotation
|
|
120
|
+
const createPendingAnnotation = (exact: string) => ({
|
|
121
|
+
motivation: 'assessing' as const,
|
|
122
|
+
selector: {
|
|
123
|
+
type: 'TextQuoteSelector' as const,
|
|
124
|
+
exact,
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe('AssessmentPanel Component', () => {
|
|
129
|
+
const defaultProps = {
|
|
130
|
+
annotations: mockAssessments.empty,
|
|
131
|
+
onAnnotationClick: vi.fn(),
|
|
132
|
+
onCreate: vi.fn(),
|
|
133
|
+
focusedAnnotationId: null,
|
|
134
|
+
pendingAnnotation: null,
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
beforeEach(() => {
|
|
138
|
+
vi.clearAllMocks();
|
|
139
|
+
|
|
140
|
+
// Mock scrollIntoView for jsdom
|
|
141
|
+
Element.prototype.scrollIntoView = vi.fn();
|
|
142
|
+
|
|
143
|
+
// Mock selector functions to return proper position data
|
|
144
|
+
mockGetTargetSelector.mockImplementation((target: any) => target.selector);
|
|
145
|
+
mockGetTextPositionSelector.mockImplementation((selector: any) => {
|
|
146
|
+
if (selector?.type === 'TextPositionSelector') {
|
|
147
|
+
return selector;
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
afterEach(() => {
|
|
154
|
+
vi.restoreAllMocks();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe('Rendering', () => {
|
|
158
|
+
it('should render panel header with title and count', () => {
|
|
159
|
+
render(<AssessmentPanel {...defaultProps} annotations={mockAssessments.multiple} />);
|
|
160
|
+
|
|
161
|
+
expect(screen.getByText(/Assessments/)).toBeInTheDocument();
|
|
162
|
+
expect(screen.getByText(/\(3\)/)).toBeInTheDocument();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should show empty state when no assessments', () => {
|
|
166
|
+
render(<AssessmentPanel {...defaultProps} />);
|
|
167
|
+
|
|
168
|
+
expect(screen.getByText(/No assessments yet/)).toBeInTheDocument();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should render all assessments', () => {
|
|
172
|
+
render(<AssessmentPanel {...defaultProps} annotations={mockAssessments.multiple} />);
|
|
173
|
+
|
|
174
|
+
expect(screen.getByTestId('assessment-1')).toBeInTheDocument();
|
|
175
|
+
expect(screen.getByTestId('assessment-2')).toBeInTheDocument();
|
|
176
|
+
expect(screen.getByTestId('assessment-3')).toBeInTheDocument();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should have proper panel structure', () => {
|
|
180
|
+
const { container } = render(<AssessmentPanel {...defaultProps} />);
|
|
181
|
+
|
|
182
|
+
const panel = container.firstChild as HTMLElement;
|
|
183
|
+
expect(panel).toHaveClass('semiont-panel');
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe('Assessment Sorting', () => {
|
|
188
|
+
it('should sort assessments by position in resource', () => {
|
|
189
|
+
render(<AssessmentPanel {...defaultProps} annotations={mockAssessments.multiple} />);
|
|
190
|
+
|
|
191
|
+
const assessments = screen.getAllByTestId(/assessment-/);
|
|
192
|
+
|
|
193
|
+
// Should be sorted by start position: assessment-2 (0), assessment-1 (50), assessment-3 (100)
|
|
194
|
+
expect(assessments[0]).toHaveAttribute('data-testid', 'assessment-2');
|
|
195
|
+
expect(assessments[1]).toHaveAttribute('data-testid', 'assessment-1');
|
|
196
|
+
expect(assessments[2]).toHaveAttribute('data-testid', 'assessment-3');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('should handle assessments without valid selectors', () => {
|
|
200
|
+
mockGetTextPositionSelector.mockReturnValue(null);
|
|
201
|
+
|
|
202
|
+
expect(() => {
|
|
203
|
+
render(<AssessmentPanel {...defaultProps} annotations={mockAssessments.multiple} />);
|
|
204
|
+
}).not.toThrow();
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe('New Assessment Creation', () => {
|
|
209
|
+
it('should not show new assessment input by default', () => {
|
|
210
|
+
render(<AssessmentPanel {...defaultProps} />);
|
|
211
|
+
|
|
212
|
+
expect(screen.queryByPlaceholderText(/Type your assessment here/)).not.toBeInTheDocument();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('should show new assessment input when pendingAnnotation exists', () => {
|
|
216
|
+
const pendingAnnotation = createPendingAnnotation('Selected text');
|
|
217
|
+
|
|
218
|
+
render(
|
|
219
|
+
<AssessmentPanel
|
|
220
|
+
{...defaultProps}
|
|
221
|
+
pendingAnnotation={pendingAnnotation}
|
|
222
|
+
/>
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
expect(screen.getByPlaceholderText(/Type your assessment here/)).toBeInTheDocument();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('should display quoted selected text in new assessment area', () => {
|
|
229
|
+
const pendingAnnotation = createPendingAnnotation('Selected text for assessment');
|
|
230
|
+
|
|
231
|
+
render(
|
|
232
|
+
<AssessmentPanel
|
|
233
|
+
{...defaultProps}
|
|
234
|
+
pendingAnnotation={pendingAnnotation}
|
|
235
|
+
/>
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
expect(screen.getByText(/"Selected text for assessment"/)).toBeInTheDocument();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('should truncate long selected text at 100 characters', () => {
|
|
242
|
+
const longText = 'A'.repeat(150);
|
|
243
|
+
const pendingAnnotation = createPendingAnnotation(longText);
|
|
244
|
+
|
|
245
|
+
render(
|
|
246
|
+
<AssessmentPanel
|
|
247
|
+
{...defaultProps}
|
|
248
|
+
pendingAnnotation={pendingAnnotation}
|
|
249
|
+
/>
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
expect(screen.getByText(new RegExp(`"${'A'.repeat(100)}`))).toBeInTheDocument();
|
|
253
|
+
expect(screen.getByText(/\.\.\./)).toBeInTheDocument();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('should allow typing in new assessment textarea', async () => {
|
|
257
|
+
const pendingAnnotation = createPendingAnnotation('Selected text');
|
|
258
|
+
|
|
259
|
+
render(
|
|
260
|
+
<AssessmentPanel
|
|
261
|
+
{...defaultProps}
|
|
262
|
+
pendingAnnotation={pendingAnnotation}
|
|
263
|
+
/>
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
const textarea = screen.getByPlaceholderText(/Type your assessment here/);
|
|
267
|
+
await userEvent.type(textarea, 'My assessment');
|
|
268
|
+
|
|
269
|
+
expect(textarea).toHaveValue('My assessment');
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('should show character count', async () => {
|
|
273
|
+
const pendingAnnotation = createPendingAnnotation('Selected text');
|
|
274
|
+
|
|
275
|
+
render(
|
|
276
|
+
<AssessmentPanel
|
|
277
|
+
{...defaultProps}
|
|
278
|
+
pendingAnnotation={pendingAnnotation}
|
|
279
|
+
/>
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
expect(screen.getByText('0/2000')).toBeInTheDocument();
|
|
283
|
+
|
|
284
|
+
const textarea = screen.getByPlaceholderText(/Type your assessment here/);
|
|
285
|
+
await userEvent.type(textarea, 'Test');
|
|
286
|
+
|
|
287
|
+
expect(screen.getByText('4/2000')).toBeInTheDocument();
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('should enforce maxLength of 2000 characters', () => {
|
|
291
|
+
const pendingAnnotation = createPendingAnnotation('Selected text');
|
|
292
|
+
|
|
293
|
+
render(
|
|
294
|
+
<AssessmentPanel
|
|
295
|
+
{...defaultProps}
|
|
296
|
+
pendingAnnotation={pendingAnnotation}
|
|
297
|
+
/>
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
const textarea = screen.getByPlaceholderText(/Type your assessment here/) as HTMLTextAreaElement;
|
|
301
|
+
expect(textarea).toHaveAttribute('maxLength', '2000');
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('should auto-focus new assessment textarea', () => {
|
|
305
|
+
const pendingAnnotation = createPendingAnnotation('Selected text');
|
|
306
|
+
|
|
307
|
+
render(
|
|
308
|
+
<AssessmentPanel
|
|
309
|
+
{...defaultProps}
|
|
310
|
+
pendingAnnotation={pendingAnnotation}
|
|
311
|
+
/>
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
const textarea = screen.getByPlaceholderText(/Type your assessment here/);
|
|
315
|
+
expect(textarea).toHaveFocus();
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('should call onCreate when save is clicked', async () => {
|
|
319
|
+
const onCreate = vi.fn();
|
|
320
|
+
const pendingAnnotation = createPendingAnnotation('Selected text');
|
|
321
|
+
|
|
322
|
+
render(
|
|
323
|
+
<AssessmentPanel
|
|
324
|
+
{...defaultProps}
|
|
325
|
+
pendingAnnotation={pendingAnnotation}
|
|
326
|
+
onCreate={onCreate}
|
|
327
|
+
/>
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
const textarea = screen.getByPlaceholderText(/Type your assessment here/);
|
|
331
|
+
await userEvent.type(textarea, 'My assessment');
|
|
332
|
+
|
|
333
|
+
const saveButton = screen.getByText('Save');
|
|
334
|
+
await userEvent.click(saveButton);
|
|
335
|
+
|
|
336
|
+
expect(onCreate).toHaveBeenCalledWith('My assessment');
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('should clear textarea after successful save', async () => {
|
|
340
|
+
const pendingAnnotation = createPendingAnnotation('Selected text');
|
|
341
|
+
|
|
342
|
+
render(
|
|
343
|
+
<AssessmentPanel
|
|
344
|
+
{...defaultProps}
|
|
345
|
+
pendingAnnotation={pendingAnnotation}
|
|
346
|
+
/>
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
const textarea = screen.getByPlaceholderText(/Type your assessment here/);
|
|
350
|
+
await userEvent.type(textarea, 'My assessment');
|
|
351
|
+
await userEvent.click(screen.getByText('Save'));
|
|
352
|
+
|
|
353
|
+
expect(textarea).toHaveValue('');
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('should allow saving with empty text (assessment text is optional)', async () => {
|
|
357
|
+
const onCreate = vi.fn();
|
|
358
|
+
const pendingAnnotation = createPendingAnnotation('Selected text');
|
|
359
|
+
|
|
360
|
+
render(
|
|
361
|
+
<AssessmentPanel
|
|
362
|
+
{...defaultProps}
|
|
363
|
+
pendingAnnotation={pendingAnnotation}
|
|
364
|
+
onCreate={onCreate}
|
|
365
|
+
/>
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
const saveButton = screen.getByText('Save');
|
|
369
|
+
await userEvent.click(saveButton);
|
|
370
|
+
|
|
371
|
+
// Should NOT call onCreate with empty text (handleSaveNewAssessment checks trim())
|
|
372
|
+
expect(onCreate).not.toHaveBeenCalled();
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('should have proper styling for new assessment area', () => {
|
|
376
|
+
const pendingAnnotation = createPendingAnnotation('Selected text');
|
|
377
|
+
|
|
378
|
+
const { container } = render(
|
|
379
|
+
<AssessmentPanel
|
|
380
|
+
{...defaultProps}
|
|
381
|
+
pendingAnnotation={pendingAnnotation}
|
|
382
|
+
/>
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
const newAssessmentArea = container.querySelector('.semiont-annotation-prompt');
|
|
386
|
+
expect(newAssessmentArea).toBeInTheDocument();
|
|
387
|
+
expect(newAssessmentArea).toHaveAttribute('data-type', 'assessment');
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
describe('Assessment Interactions', () => {
|
|
392
|
+
it('should call onAnnotationClick when assessment is clicked', () => {
|
|
393
|
+
const onAnnotationClick = vi.fn();
|
|
394
|
+
render(
|
|
395
|
+
<AssessmentPanel
|
|
396
|
+
{...defaultProps}
|
|
397
|
+
annotations={mockAssessments.single}
|
|
398
|
+
onAnnotationClick={onAnnotationClick}
|
|
399
|
+
/>
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
const assessment = screen.getByTestId('assessment-1');
|
|
403
|
+
fireEvent.click(assessment);
|
|
404
|
+
|
|
405
|
+
expect(onAnnotationClick).toHaveBeenCalledWith(mockAssessments.single[0]);
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
describe('Assessment Hover Behavior', () => {
|
|
410
|
+
it('should call onAnnotationHover when provided', () => {
|
|
411
|
+
const onAnnotationHover = vi.fn();
|
|
412
|
+
render(
|
|
413
|
+
<AssessmentPanel
|
|
414
|
+
{...defaultProps}
|
|
415
|
+
annotations={mockAssessments.single}
|
|
416
|
+
onAnnotationHover={onAnnotationHover}
|
|
417
|
+
/>
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
const hoverButton = screen.getByText('Hover');
|
|
421
|
+
fireEvent.mouseEnter(hoverButton);
|
|
422
|
+
|
|
423
|
+
expect(onAnnotationHover).toHaveBeenCalledWith('1');
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it('should not error when onAnnotationHover is not provided', () => {
|
|
427
|
+
expect(() => {
|
|
428
|
+
render(
|
|
429
|
+
<AssessmentPanel
|
|
430
|
+
{...defaultProps}
|
|
431
|
+
annotations={mockAssessments.single}
|
|
432
|
+
/>
|
|
433
|
+
);
|
|
434
|
+
}).not.toThrow();
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
describe('Detection Section', () => {
|
|
439
|
+
it('should render DetectSection when onDetect is provided and annotateMode is true', () => {
|
|
440
|
+
render(
|
|
441
|
+
<AssessmentPanel
|
|
442
|
+
{...defaultProps}
|
|
443
|
+
onDetect={vi.fn()}
|
|
444
|
+
annotateMode={true}
|
|
445
|
+
/>
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
expect(screen.getByTestId('detect-section')).toBeInTheDocument();
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
it('should not render DetectSection when onDetect is not provided', () => {
|
|
452
|
+
render(
|
|
453
|
+
<AssessmentPanel
|
|
454
|
+
{...defaultProps}
|
|
455
|
+
annotateMode={true}
|
|
456
|
+
/>
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
expect(screen.queryByTestId('detect-section')).not.toBeInTheDocument();
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it('should not render DetectSection when annotateMode is false', () => {
|
|
463
|
+
render(
|
|
464
|
+
<AssessmentPanel
|
|
465
|
+
{...defaultProps}
|
|
466
|
+
onDetect={vi.fn()}
|
|
467
|
+
annotateMode={false}
|
|
468
|
+
/>
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
expect(screen.queryByTestId('detect-section')).not.toBeInTheDocument();
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it('should call onDetect when detection is started', async () => {
|
|
475
|
+
const onDetect = vi.fn();
|
|
476
|
+
render(
|
|
477
|
+
<AssessmentPanel
|
|
478
|
+
{...defaultProps}
|
|
479
|
+
onDetect={onDetect}
|
|
480
|
+
annotateMode={true}
|
|
481
|
+
/>
|
|
482
|
+
);
|
|
483
|
+
|
|
484
|
+
const detectButton = screen.getByText('Start Detection');
|
|
485
|
+
await userEvent.click(detectButton);
|
|
486
|
+
|
|
487
|
+
expect(onDetect).toHaveBeenCalledWith('test instructions');
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
describe('Cancel Functionality', () => {
|
|
492
|
+
it('should show Cancel button when pendingAnnotation exists', () => {
|
|
493
|
+
const pendingAnnotation = createPendingAnnotation('Selected text');
|
|
494
|
+
|
|
495
|
+
render(
|
|
496
|
+
<AssessmentPanel
|
|
497
|
+
{...defaultProps}
|
|
498
|
+
pendingAnnotation={pendingAnnotation}
|
|
499
|
+
/>
|
|
500
|
+
);
|
|
501
|
+
|
|
502
|
+
expect(screen.getByText('Cancel')).toBeInTheDocument();
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it('should clear textarea when Cancel button is clicked', async () => {
|
|
506
|
+
const pendingAnnotation = createPendingAnnotation('Selected text');
|
|
507
|
+
|
|
508
|
+
render(
|
|
509
|
+
<AssessmentPanel
|
|
510
|
+
{...defaultProps}
|
|
511
|
+
pendingAnnotation={pendingAnnotation}
|
|
512
|
+
/>
|
|
513
|
+
);
|
|
514
|
+
|
|
515
|
+
const textarea = screen.getByPlaceholderText(/Type your assessment here/);
|
|
516
|
+
await userEvent.type(textarea, 'My assessment');
|
|
517
|
+
|
|
518
|
+
const cancelButton = screen.getByText('Cancel');
|
|
519
|
+
await userEvent.click(cancelButton);
|
|
520
|
+
|
|
521
|
+
expect(textarea).toHaveValue('');
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
describe('Accessibility', () => {
|
|
526
|
+
it('should have proper heading structure', () => {
|
|
527
|
+
render(<AssessmentPanel {...defaultProps} />);
|
|
528
|
+
|
|
529
|
+
const heading = screen.getByText(/Assessments/);
|
|
530
|
+
expect(heading).toHaveClass('semiont-panel-header__text');
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
it('should have proper textarea attributes for new assessments', () => {
|
|
534
|
+
const pendingAnnotation = createPendingAnnotation('Selected text');
|
|
535
|
+
|
|
536
|
+
render(
|
|
537
|
+
<AssessmentPanel
|
|
538
|
+
{...defaultProps}
|
|
539
|
+
pendingAnnotation={pendingAnnotation}
|
|
540
|
+
/>
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
const textarea = screen.getByPlaceholderText(/Type your assessment here/);
|
|
544
|
+
expect(textarea).toHaveAttribute('rows', '3');
|
|
545
|
+
});
|
|
546
|
+
});
|
|
547
|
+
});
|
|
@@ -9,6 +9,15 @@ import type { components } from '@semiont/api-client';
|
|
|
9
9
|
|
|
10
10
|
type Annotation = components['schemas']['Annotation'];
|
|
11
11
|
|
|
12
|
+
// Mock MakeMeaningEventBusContext
|
|
13
|
+
vi.mock('../../../../contexts/MakeMeaningEventBusContext', () => ({
|
|
14
|
+
useMakeMeaningEvents: vi.fn(() => ({
|
|
15
|
+
emit: vi.fn(),
|
|
16
|
+
on: vi.fn(),
|
|
17
|
+
off: vi.fn(),
|
|
18
|
+
})),
|
|
19
|
+
}));
|
|
20
|
+
|
|
12
21
|
// Mock TranslationContext
|
|
13
22
|
vi.mock('../../../../contexts/TranslationContext', () => ({
|
|
14
23
|
useTranslations: vi.fn(() => (key: string) => {
|
|
@@ -17,6 +26,7 @@ vi.mock('../../../../contexts/TranslationContext', () => ({
|
|
|
17
26
|
noComments: 'No comments yet. Select text to add a comment.',
|
|
18
27
|
commentPlaceholder: 'Add your comment...',
|
|
19
28
|
save: 'Save',
|
|
29
|
+
cancel: 'Cancel',
|
|
20
30
|
};
|
|
21
31
|
return translations[key] || key;
|
|
22
32
|
}),
|