@semiont/react-ui 0.2.33-build.77 → 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
|
@@ -5,6 +5,15 @@ import userEvent from '@testing-library/user-event';
|
|
|
5
5
|
import '@testing-library/jest-dom';
|
|
6
6
|
import { ReferencesPanel } from '../ReferencesPanel';
|
|
7
7
|
|
|
8
|
+
// Mock MakeMeaningEventBusContext
|
|
9
|
+
vi.mock('../../../../contexts/MakeMeaningEventBusContext', () => ({
|
|
10
|
+
useMakeMeaningEvents: vi.fn(() => ({
|
|
11
|
+
emit: vi.fn(),
|
|
12
|
+
on: vi.fn(),
|
|
13
|
+
off: vi.fn(),
|
|
14
|
+
})),
|
|
15
|
+
}));
|
|
16
|
+
|
|
8
17
|
// Mock TranslationContext
|
|
9
18
|
vi.mock('../../../../contexts/TranslationContext', () => ({
|
|
10
19
|
useTranslations: vi.fn(() => (key: string, params?: Record<string, any>) => {
|
|
@@ -20,6 +29,7 @@ vi.mock('../../../../contexts/TranslationContext', () => ({
|
|
|
20
29
|
more: 'Detect More',
|
|
21
30
|
includeDescriptiveReferences: 'Include descriptive references',
|
|
22
31
|
descriptiveReferencesTooltip: 'Also find phrases like \'the CEO\', \'the tech giant\', \'the physicist\' (in addition to names)',
|
|
32
|
+
cancel: 'Cancel',
|
|
23
33
|
};
|
|
24
34
|
let result = translations[key] || key;
|
|
25
35
|
// Replace {count} with actual count value if provided
|
|
@@ -0,0 +1,564 @@
|
|
|
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 { TaggingPanel } from '../TaggingPanel';
|
|
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, params?: Record<string, any>) => {
|
|
24
|
+
const translations: Record<string, string> = {
|
|
25
|
+
title: 'Tags',
|
|
26
|
+
noTags: 'No tags yet. Select text to add a tag.',
|
|
27
|
+
createTagForSelection: 'Create tag for selection',
|
|
28
|
+
selectSchema: 'Select schema',
|
|
29
|
+
selectCategory: 'Select category',
|
|
30
|
+
selectCategories: 'Select categories',
|
|
31
|
+
chooseCategory: 'Choose a category',
|
|
32
|
+
schemaLegal: 'Legal (IRAC)',
|
|
33
|
+
schemaScientific: 'Scientific (IMRAD)',
|
|
34
|
+
schemaArgument: 'Argument',
|
|
35
|
+
detectTags: 'Detect Tags',
|
|
36
|
+
detect: 'Detect',
|
|
37
|
+
cancel: 'Cancel',
|
|
38
|
+
fragmentSelected: 'Fragment selected',
|
|
39
|
+
selectAll: 'Select All',
|
|
40
|
+
deselectAll: 'Deselect All',
|
|
41
|
+
categoriesSelected: '{count} categories selected',
|
|
42
|
+
categoryIssue: 'Issue',
|
|
43
|
+
categoryRule: 'Rule',
|
|
44
|
+
categoryApplication: 'Application',
|
|
45
|
+
categoryConclusion: 'Conclusion',
|
|
46
|
+
};
|
|
47
|
+
let result = translations[key] || key;
|
|
48
|
+
if (params?.count !== undefined) {
|
|
49
|
+
result = result.replace('{count}', String(params.count));
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}),
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
// Mock @semiont/api-client utilities
|
|
56
|
+
vi.mock('@semiont/api-client', async () => {
|
|
57
|
+
const actual = await vi.importActual('@semiont/api-client');
|
|
58
|
+
return {
|
|
59
|
+
...actual,
|
|
60
|
+
getTextPositionSelector: vi.fn(),
|
|
61
|
+
getTargetSelector: vi.fn(),
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Mock TagEntry component to simplify testing
|
|
66
|
+
vi.mock('../TagEntry', () => ({
|
|
67
|
+
TagEntry: ({ tag, onClick, onTagRef, onTagHover }: any) => (
|
|
68
|
+
<div
|
|
69
|
+
data-testid={`tag-${tag.id}`}
|
|
70
|
+
onClick={() => onClick()}
|
|
71
|
+
>
|
|
72
|
+
<button
|
|
73
|
+
onMouseEnter={() => onTagHover?.(tag.id)}
|
|
74
|
+
onMouseLeave={() => onTagHover?.(null)}
|
|
75
|
+
>
|
|
76
|
+
Hover
|
|
77
|
+
</button>
|
|
78
|
+
<div>{tag.id}</div>
|
|
79
|
+
</div>
|
|
80
|
+
),
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
// Mock tag schemas
|
|
84
|
+
vi.mock('../../../../lib/tag-schemas', () => ({
|
|
85
|
+
getAllTagSchemas: vi.fn(() => [
|
|
86
|
+
{
|
|
87
|
+
id: 'legal-irac',
|
|
88
|
+
name: 'Legal (IRAC)',
|
|
89
|
+
description: 'Issue, Rule, Application, Conclusion framework for legal analysis',
|
|
90
|
+
tags: [
|
|
91
|
+
{ name: 'Issue', description: 'Legal question to be resolved', color: '#3b82f6' },
|
|
92
|
+
{ name: 'Rule', description: 'Legal principle or statute', color: '#10b981' },
|
|
93
|
+
{ name: 'Application', description: 'Application of rule to facts', color: '#f59e0b' },
|
|
94
|
+
{ name: 'Conclusion', description: 'Resolution of the issue', color: '#ef4444' },
|
|
95
|
+
],
|
|
96
|
+
},
|
|
97
|
+
]),
|
|
98
|
+
}));
|
|
99
|
+
|
|
100
|
+
import { getTextPositionSelector, getTargetSelector } from '@semiont/api-client';
|
|
101
|
+
|
|
102
|
+
const mockGetTextPositionSelector = getTextPositionSelector as MockedFunction<typeof getTextPositionSelector>;
|
|
103
|
+
const mockGetTargetSelector = getTargetSelector as MockedFunction<typeof getTargetSelector>;
|
|
104
|
+
|
|
105
|
+
// Test data fixtures
|
|
106
|
+
const createMockTag = (id: string, start: number, end: number, tagName: string = 'Issue'): Annotation => ({
|
|
107
|
+
'@context': 'http://www.w3.org/ns/anno.jsonld',
|
|
108
|
+
id,
|
|
109
|
+
type: 'Annotation',
|
|
110
|
+
motivation: 'tagging',
|
|
111
|
+
creator: {
|
|
112
|
+
name: `user${id}@example.com`,
|
|
113
|
+
},
|
|
114
|
+
created: `2024-01-0${id.slice(-1)}T10:00:00Z`,
|
|
115
|
+
modified: `2024-01-0${id.slice(-1)}T10:00:00Z`,
|
|
116
|
+
target: {
|
|
117
|
+
source: 'resource-1',
|
|
118
|
+
selector: {
|
|
119
|
+
type: 'TextPositionSelector',
|
|
120
|
+
start,
|
|
121
|
+
end,
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
body: [
|
|
125
|
+
{
|
|
126
|
+
type: 'TextualBody',
|
|
127
|
+
value: tagName,
|
|
128
|
+
purpose: 'tagging',
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const mockTags = {
|
|
134
|
+
empty: [],
|
|
135
|
+
single: [createMockTag('1', 0, 10)],
|
|
136
|
+
multiple: [
|
|
137
|
+
createMockTag('1', 50, 60, 'Issue'),
|
|
138
|
+
createMockTag('2', 0, 10, 'Rule'),
|
|
139
|
+
createMockTag('3', 100, 110, 'Conclusion'),
|
|
140
|
+
],
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// Helper to create pending annotation
|
|
144
|
+
const createPendingAnnotation = (exact: string) => ({
|
|
145
|
+
motivation: 'tagging' as const,
|
|
146
|
+
selector: {
|
|
147
|
+
type: 'TextQuoteSelector' as const,
|
|
148
|
+
exact,
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe('TaggingPanel Component', () => {
|
|
153
|
+
const defaultProps = {
|
|
154
|
+
annotations: mockTags.empty,
|
|
155
|
+
onAnnotationClick: vi.fn(),
|
|
156
|
+
onCreate: vi.fn(),
|
|
157
|
+
focusedAnnotationId: null,
|
|
158
|
+
pendingAnnotation: null,
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
beforeEach(() => {
|
|
162
|
+
vi.clearAllMocks();
|
|
163
|
+
|
|
164
|
+
// Mock scrollIntoView for jsdom
|
|
165
|
+
Element.prototype.scrollIntoView = vi.fn();
|
|
166
|
+
|
|
167
|
+
// Mock selector functions to return proper position data
|
|
168
|
+
mockGetTargetSelector.mockImplementation((target: any) => target.selector);
|
|
169
|
+
mockGetTextPositionSelector.mockImplementation((selector: any) => {
|
|
170
|
+
if (selector?.type === 'TextPositionSelector') {
|
|
171
|
+
return selector;
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Mock localStorage
|
|
177
|
+
Storage.prototype.getItem = vi.fn(() => 'true');
|
|
178
|
+
Storage.prototype.setItem = vi.fn();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
afterEach(() => {
|
|
182
|
+
vi.restoreAllMocks();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe('Rendering', () => {
|
|
186
|
+
it('should render panel header with title and count', () => {
|
|
187
|
+
render(<TaggingPanel {...defaultProps} annotations={mockTags.multiple} />);
|
|
188
|
+
|
|
189
|
+
expect(screen.getByText(/Tags/)).toBeInTheDocument();
|
|
190
|
+
expect(screen.getByText(/\(3\)/)).toBeInTheDocument();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should show empty state when no tags', () => {
|
|
194
|
+
render(<TaggingPanel {...defaultProps} />);
|
|
195
|
+
|
|
196
|
+
expect(screen.getByText(/No tags yet/)).toBeInTheDocument();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('should render all tags', () => {
|
|
200
|
+
render(<TaggingPanel {...defaultProps} annotations={mockTags.multiple} />);
|
|
201
|
+
|
|
202
|
+
expect(screen.getByTestId('tag-1')).toBeInTheDocument();
|
|
203
|
+
expect(screen.getByTestId('tag-2')).toBeInTheDocument();
|
|
204
|
+
expect(screen.getByTestId('tag-3')).toBeInTheDocument();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('should have proper panel structure', () => {
|
|
208
|
+
const { container } = render(<TaggingPanel {...defaultProps} />);
|
|
209
|
+
|
|
210
|
+
const panel = container.firstChild as HTMLElement;
|
|
211
|
+
expect(panel).toHaveClass('semiont-panel');
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe('Tag Sorting', () => {
|
|
216
|
+
it('should sort tags by position in resource', () => {
|
|
217
|
+
render(<TaggingPanel {...defaultProps} annotations={mockTags.multiple} />);
|
|
218
|
+
|
|
219
|
+
const tags = screen.getAllByTestId(/tag-/);
|
|
220
|
+
|
|
221
|
+
// Should be sorted by start position: tag-2 (0), tag-1 (50), tag-3 (100)
|
|
222
|
+
expect(tags[0]).toHaveAttribute('data-testid', 'tag-2');
|
|
223
|
+
expect(tags[1]).toHaveAttribute('data-testid', 'tag-1');
|
|
224
|
+
expect(tags[2]).toHaveAttribute('data-testid', 'tag-3');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should handle tags without valid selectors', () => {
|
|
228
|
+
mockGetTextPositionSelector.mockReturnValue(null);
|
|
229
|
+
|
|
230
|
+
expect(() => {
|
|
231
|
+
render(<TaggingPanel {...defaultProps} annotations={mockTags.multiple} />);
|
|
232
|
+
}).not.toThrow();
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
describe('Manual Tag Creation', () => {
|
|
237
|
+
it('should not show tag creation form by default', () => {
|
|
238
|
+
render(<TaggingPanel {...defaultProps} />);
|
|
239
|
+
|
|
240
|
+
expect(screen.queryByText(/Create tag for selection/)).not.toBeInTheDocument();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('should show tag creation form when pendingAnnotation exists', () => {
|
|
244
|
+
const pendingAnnotation = createPendingAnnotation('Selected text');
|
|
245
|
+
|
|
246
|
+
render(
|
|
247
|
+
<TaggingPanel
|
|
248
|
+
{...defaultProps}
|
|
249
|
+
pendingAnnotation={pendingAnnotation}
|
|
250
|
+
/>
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
expect(screen.getByText(/Create tag for selection/)).toBeInTheDocument();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('should display quoted selected text in tag creation form', () => {
|
|
257
|
+
const pendingAnnotation = createPendingAnnotation('Selected text for tagging');
|
|
258
|
+
|
|
259
|
+
render(
|
|
260
|
+
<TaggingPanel
|
|
261
|
+
{...defaultProps}
|
|
262
|
+
pendingAnnotation={pendingAnnotation}
|
|
263
|
+
/>
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
expect(screen.getByText(/"Selected text for tagging"/)).toBeInTheDocument();
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('should truncate long selected text at 100 characters', () => {
|
|
270
|
+
const longText = 'A'.repeat(150);
|
|
271
|
+
const pendingAnnotation = createPendingAnnotation(longText);
|
|
272
|
+
|
|
273
|
+
render(
|
|
274
|
+
<TaggingPanel
|
|
275
|
+
{...defaultProps}
|
|
276
|
+
pendingAnnotation={pendingAnnotation}
|
|
277
|
+
/>
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
expect(screen.getByText(new RegExp(`"${'A'.repeat(100)}`))).toBeInTheDocument();
|
|
281
|
+
expect(screen.getByText(/\.\.\./)).toBeInTheDocument();
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('should show schema selector in tag creation form', () => {
|
|
285
|
+
const pendingAnnotation = createPendingAnnotation('Selected text');
|
|
286
|
+
|
|
287
|
+
render(
|
|
288
|
+
<TaggingPanel
|
|
289
|
+
{...defaultProps}
|
|
290
|
+
pendingAnnotation={pendingAnnotation}
|
|
291
|
+
/>
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
const selects = screen.getAllByText(/Select schema/);
|
|
295
|
+
expect(selects.length).toBeGreaterThan(0);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('should show category selector in tag creation form', () => {
|
|
299
|
+
const pendingAnnotation = createPendingAnnotation('Selected text');
|
|
300
|
+
|
|
301
|
+
render(
|
|
302
|
+
<TaggingPanel
|
|
303
|
+
{...defaultProps}
|
|
304
|
+
pendingAnnotation={pendingAnnotation}
|
|
305
|
+
/>
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
expect(screen.getByText(/Select category/)).toBeInTheDocument();
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('should call onCreate when category is selected', async () => {
|
|
312
|
+
const onCreate = vi.fn();
|
|
313
|
+
const pendingAnnotation = createPendingAnnotation('Selected text');
|
|
314
|
+
|
|
315
|
+
render(
|
|
316
|
+
<TaggingPanel
|
|
317
|
+
{...defaultProps}
|
|
318
|
+
pendingAnnotation={pendingAnnotation}
|
|
319
|
+
onCreate={onCreate}
|
|
320
|
+
/>
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
// Find the category selector (the one in the pending annotation form)
|
|
324
|
+
const categorySelects = screen.getAllByRole('combobox');
|
|
325
|
+
const categorySelect = categorySelects.find(select =>
|
|
326
|
+
select.querySelector('option[value=""]')?.textContent === 'Choose a category'
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
expect(categorySelect).toBeInTheDocument();
|
|
330
|
+
|
|
331
|
+
await userEvent.selectOptions(categorySelect!, 'Issue');
|
|
332
|
+
|
|
333
|
+
expect(onCreate).toHaveBeenCalledWith('legal-irac', 'Issue');
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('should have proper styling for tag creation form', () => {
|
|
337
|
+
const pendingAnnotation = createPendingAnnotation('Selected text');
|
|
338
|
+
|
|
339
|
+
const { container } = render(
|
|
340
|
+
<TaggingPanel
|
|
341
|
+
{...defaultProps}
|
|
342
|
+
pendingAnnotation={pendingAnnotation}
|
|
343
|
+
/>
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
const tagForm = container.querySelector('.semiont-annotation-prompt');
|
|
347
|
+
expect(tagForm).toBeInTheDocument();
|
|
348
|
+
expect(tagForm).toHaveAttribute('data-type', 'tag');
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
describe('Tag Interactions', () => {
|
|
353
|
+
it('should call onAnnotationClick when tag is clicked', () => {
|
|
354
|
+
const onAnnotationClick = vi.fn();
|
|
355
|
+
render(
|
|
356
|
+
<TaggingPanel
|
|
357
|
+
{...defaultProps}
|
|
358
|
+
annotations={mockTags.single}
|
|
359
|
+
onAnnotationClick={onAnnotationClick}
|
|
360
|
+
/>
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
const tag = screen.getByTestId('tag-1');
|
|
364
|
+
fireEvent.click(tag);
|
|
365
|
+
|
|
366
|
+
expect(onAnnotationClick).toHaveBeenCalledWith(mockTags.single[0]);
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
describe('Tag Hover Behavior', () => {
|
|
371
|
+
it('should call onAnnotationHover when provided', () => {
|
|
372
|
+
const onAnnotationHover = vi.fn();
|
|
373
|
+
render(
|
|
374
|
+
<TaggingPanel
|
|
375
|
+
{...defaultProps}
|
|
376
|
+
annotations={mockTags.single}
|
|
377
|
+
onAnnotationHover={onAnnotationHover}
|
|
378
|
+
/>
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
const hoverButton = screen.getByText('Hover');
|
|
382
|
+
fireEvent.mouseEnter(hoverButton);
|
|
383
|
+
|
|
384
|
+
expect(onAnnotationHover).toHaveBeenCalledWith('1');
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it('should not error when onAnnotationHover is not provided', () => {
|
|
388
|
+
expect(() => {
|
|
389
|
+
render(
|
|
390
|
+
<TaggingPanel
|
|
391
|
+
{...defaultProps}
|
|
392
|
+
annotations={mockTags.single}
|
|
393
|
+
/>
|
|
394
|
+
);
|
|
395
|
+
}).not.toThrow();
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
describe('Detection Section', () => {
|
|
400
|
+
it('should render detection section when onDetect is provided and annotateMode is true', () => {
|
|
401
|
+
render(
|
|
402
|
+
<TaggingPanel
|
|
403
|
+
{...defaultProps}
|
|
404
|
+
onDetect={vi.fn()}
|
|
405
|
+
annotateMode={true}
|
|
406
|
+
/>
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
expect(screen.getByText(/Detect Tags/)).toBeInTheDocument();
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it('should not render detection section when onDetect is not provided', () => {
|
|
413
|
+
render(
|
|
414
|
+
<TaggingPanel
|
|
415
|
+
{...defaultProps}
|
|
416
|
+
annotateMode={true}
|
|
417
|
+
/>
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
expect(screen.queryByText(/Detect Tags/)).not.toBeInTheDocument();
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it('should not render detection section when annotateMode is false', () => {
|
|
424
|
+
render(
|
|
425
|
+
<TaggingPanel
|
|
426
|
+
{...defaultProps}
|
|
427
|
+
onDetect={vi.fn()}
|
|
428
|
+
annotateMode={false}
|
|
429
|
+
/>
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
expect(screen.queryByText(/Detect Tags/)).not.toBeInTheDocument();
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it('should show schema selector in detection section', () => {
|
|
436
|
+
render(
|
|
437
|
+
<TaggingPanel
|
|
438
|
+
{...defaultProps}
|
|
439
|
+
onDetect={vi.fn()}
|
|
440
|
+
annotateMode={true}
|
|
441
|
+
/>
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
const selects = screen.getAllByText(/Select schema/);
|
|
445
|
+
expect(selects.length).toBeGreaterThan(0);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it('should show Select All and Deselect All buttons', () => {
|
|
449
|
+
render(
|
|
450
|
+
<TaggingPanel
|
|
451
|
+
{...defaultProps}
|
|
452
|
+
onDetect={vi.fn()}
|
|
453
|
+
annotateMode={true}
|
|
454
|
+
/>
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
expect(screen.getByText('Select All')).toBeInTheDocument();
|
|
458
|
+
expect(screen.getByText('Deselect All')).toBeInTheDocument();
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it('should show category checkboxes', () => {
|
|
462
|
+
render(
|
|
463
|
+
<TaggingPanel
|
|
464
|
+
{...defaultProps}
|
|
465
|
+
onDetect={vi.fn()}
|
|
466
|
+
annotateMode={true}
|
|
467
|
+
/>
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
expect(screen.getByText('Issue')).toBeInTheDocument();
|
|
471
|
+
expect(screen.getByText('Rule')).toBeInTheDocument();
|
|
472
|
+
expect(screen.getByText('Application')).toBeInTheDocument();
|
|
473
|
+
expect(screen.getByText('Conclusion')).toBeInTheDocument();
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it('should disable detect button when no categories selected', () => {
|
|
477
|
+
render(
|
|
478
|
+
<TaggingPanel
|
|
479
|
+
{...defaultProps}
|
|
480
|
+
onDetect={vi.fn()}
|
|
481
|
+
annotateMode={true}
|
|
482
|
+
/>
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
const detectButton = screen.getByRole('button', { name: /✨ Detect/i });
|
|
486
|
+
expect(detectButton).toBeDisabled();
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it('should enable detect button when categories are selected', async () => {
|
|
490
|
+
render(
|
|
491
|
+
<TaggingPanel
|
|
492
|
+
{...defaultProps}
|
|
493
|
+
onDetect={vi.fn()}
|
|
494
|
+
annotateMode={true}
|
|
495
|
+
/>
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
const issueCheckbox = screen.getByLabelText(/Issue/);
|
|
499
|
+
await userEvent.click(issueCheckbox);
|
|
500
|
+
|
|
501
|
+
const detectButton = screen.getByRole('button', { name: /✨ Detect/i });
|
|
502
|
+
expect(detectButton).not.toBeDisabled();
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it('should call onDetect with selected schema and categories', async () => {
|
|
506
|
+
const onDetect = vi.fn();
|
|
507
|
+
render(
|
|
508
|
+
<TaggingPanel
|
|
509
|
+
{...defaultProps}
|
|
510
|
+
onDetect={onDetect}
|
|
511
|
+
annotateMode={true}
|
|
512
|
+
/>
|
|
513
|
+
);
|
|
514
|
+
|
|
515
|
+
const issueCheckbox = screen.getByLabelText(/Issue/);
|
|
516
|
+
const ruleCheckbox = screen.getByLabelText(/Rule/);
|
|
517
|
+
|
|
518
|
+
await userEvent.click(issueCheckbox);
|
|
519
|
+
await userEvent.click(ruleCheckbox);
|
|
520
|
+
|
|
521
|
+
const detectButton = screen.getByRole('button', { name: /✨ Detect/i });
|
|
522
|
+
await userEvent.click(detectButton);
|
|
523
|
+
|
|
524
|
+
expect(onDetect).toHaveBeenCalledWith('legal-irac', ['Issue', 'Rule']);
|
|
525
|
+
});
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
describe('Cancel Functionality', () => {
|
|
529
|
+
it('should show Cancel button when pendingAnnotation exists', () => {
|
|
530
|
+
const pendingAnnotation = createPendingAnnotation('Selected text');
|
|
531
|
+
|
|
532
|
+
render(
|
|
533
|
+
<TaggingPanel
|
|
534
|
+
{...defaultProps}
|
|
535
|
+
pendingAnnotation={pendingAnnotation}
|
|
536
|
+
/>
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
expect(screen.getByText('Cancel')).toBeInTheDocument();
|
|
540
|
+
});
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
describe('Accessibility', () => {
|
|
544
|
+
it('should have proper heading structure', () => {
|
|
545
|
+
render(<TaggingPanel {...defaultProps} />);
|
|
546
|
+
|
|
547
|
+
const heading = screen.getByText(/Tags/);
|
|
548
|
+
expect(heading).toHaveClass('semiont-panel-header__text');
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it('should have proper checkbox labels', () => {
|
|
552
|
+
render(
|
|
553
|
+
<TaggingPanel
|
|
554
|
+
{...defaultProps}
|
|
555
|
+
onDetect={vi.fn()}
|
|
556
|
+
annotateMode={true}
|
|
557
|
+
/>
|
|
558
|
+
);
|
|
559
|
+
|
|
560
|
+
expect(screen.getByLabelText(/Issue/)).toBeInTheDocument();
|
|
561
|
+
expect(screen.getByLabelText(/Rule/)).toBeInTheDocument();
|
|
562
|
+
});
|
|
563
|
+
});
|
|
564
|
+
});
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* All dependencies passed as props - no Next.js hooks!
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import React, { useState,
|
|
8
|
+
import React, { useState, useCallback } from 'react';
|
|
9
9
|
import type { components } from '@semiont/api-client';
|
|
10
10
|
import { getResourceId } from '@semiont/api-client';
|
|
11
11
|
import { useRovingTabIndex, Toolbar } from '@semiont/react-ui';
|
|
@@ -81,20 +81,13 @@ export function ResourceDiscoveryPage({
|
|
|
81
81
|
const hasSearchQuery = searchQuery.trim() !== '';
|
|
82
82
|
const hasSearchResults = searchDocuments.length > 0;
|
|
83
83
|
|
|
84
|
-
//
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (!selectedEntityType) return baseDocuments;
|
|
93
|
-
|
|
94
|
-
return baseDocuments.filter((resource: ResourceDescriptor) =>
|
|
95
|
-
resource.entityTypes && resource.entityTypes.includes(selectedEntityType)
|
|
96
|
-
);
|
|
97
|
-
}, [recentDocuments, searchDocuments, selectedEntityType, hasSearchResults]);
|
|
84
|
+
// Filtered documents
|
|
85
|
+
const baseDocuments = hasSearchResults ? searchDocuments : recentDocuments;
|
|
86
|
+
const filteredResources = !selectedEntityType
|
|
87
|
+
? baseDocuments
|
|
88
|
+
: baseDocuments.filter((resource: ResourceDescriptor) =>
|
|
89
|
+
resource.entityTypes && resource.entityTypes.includes(selectedEntityType)
|
|
90
|
+
);
|
|
98
91
|
|
|
99
92
|
// Roving tabindex for entity type filters
|
|
100
93
|
const entityFilterRoving = useRovingTabIndex<HTMLDivElement>(
|
|
@@ -11,15 +11,20 @@ import { ResourceViewerPage } from '../components/ResourceViewerPage';
|
|
|
11
11
|
import type { ResourceViewerPageProps } from '../components/ResourceViewerPage';
|
|
12
12
|
|
|
13
13
|
// Mock dependencies that ResourceViewerPage imports
|
|
14
|
-
vi.mock('@tanstack/react-query', () =>
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
vi.mock('@tanstack/react-query', async () => {
|
|
15
|
+
const actual = await vi.importActual('@tanstack/react-query');
|
|
16
|
+
return {
|
|
17
|
+
...actual,
|
|
18
|
+
useQueryClient: () => ({
|
|
19
|
+
invalidateQueries: vi.fn(),
|
|
20
|
+
setQueryData: vi.fn(),
|
|
21
|
+
}),
|
|
22
|
+
};
|
|
23
|
+
});
|
|
20
24
|
|
|
21
25
|
vi.mock('@semiont/react-ui', async () => {
|
|
22
26
|
const actual = await vi.importActual('@semiont/react-ui');
|
|
27
|
+
const mitt = await import('mitt');
|
|
23
28
|
return {
|
|
24
29
|
...actual,
|
|
25
30
|
ResourceViewer: ({ resource }: any) => <div data-testid="resource-viewer">{resource.name}</div>,
|
|
@@ -39,6 +44,8 @@ vi.mock('@semiont/react-ui', async () => {
|
|
|
39
44
|
}),
|
|
40
45
|
useDebouncedCallback: (fn: any) => fn,
|
|
41
46
|
supportsDetection: () => false,
|
|
47
|
+
MakeMeaningEventBusProvider: ({ children }: any) => children,
|
|
48
|
+
useMakeMeaningEvents: () => mitt.default(),
|
|
42
49
|
};
|
|
43
50
|
});
|
|
44
51
|
|