@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.
Files changed (147) hide show
  1. package/dist/{ar-RNNSPLQB.mjs → ar-EMHEHPCJ.mjs} +2 -1
  2. package/dist/ar-EMHEHPCJ.mjs.map +1 -0
  3. package/dist/{bn-S2CDL7EC.mjs → bn-OVCI4F6X.mjs} +2 -1
  4. package/dist/bn-OVCI4F6X.mjs.map +1 -0
  5. package/dist/{chunk-35LLVRFK.mjs → chunk-JZIO2A3B.mjs} +31 -31
  6. package/dist/{chunk-UDX2Q35T.mjs → chunk-LIHZTECW.mjs} +2 -1
  7. package/dist/chunk-LIHZTECW.mjs.map +1 -0
  8. package/dist/{cs-RSV675WU.mjs → cs-FAN66Q2F.mjs} +2 -1
  9. package/dist/cs-FAN66Q2F.mjs.map +1 -0
  10. package/dist/{da-CHXNPWJC.mjs → da-YBBIHI2O.mjs} +2 -1
  11. package/dist/da-YBBIHI2O.mjs.map +1 -0
  12. package/dist/{de-KPEZ53D4.mjs → de-MAYU33LB.mjs} +2 -1
  13. package/dist/de-MAYU33LB.mjs.map +1 -0
  14. package/dist/{el-MW2BME5T.mjs → el-MKGSWN4O.mjs} +2 -1
  15. package/dist/el-MKGSWN4O.mjs.map +1 -0
  16. package/dist/{en-EVMIX24Y.mjs → en-DDLIXJCU.mjs} +2 -2
  17. package/dist/{es-HQ24NYS3.mjs → es-52LHUWJD.mjs} +2 -1
  18. package/dist/es-52LHUWJD.mjs.map +1 -0
  19. package/dist/{fa-W34LRLHG.mjs → fa-FJICRANB.mjs} +2 -1
  20. package/dist/fa-FJICRANB.mjs.map +1 -0
  21. package/dist/{fi-3U44IGOA.mjs → fi-O455XFCR.mjs} +2 -1
  22. package/dist/fi-O455XFCR.mjs.map +1 -0
  23. package/dist/{fr-N7DKX6NN.mjs → fr-TXIXHOOE.mjs} +2 -1
  24. package/dist/fr-TXIXHOOE.mjs.map +1 -0
  25. package/dist/{he-CS4WRXN3.mjs → he-JBSOX5IN.mjs} +2 -1
  26. package/dist/he-JBSOX5IN.mjs.map +1 -0
  27. package/dist/{hi-GJDY46KA.mjs → hi-KGHI3XVT.mjs} +2 -1
  28. package/dist/hi-KGHI3XVT.mjs.map +1 -0
  29. package/dist/{id-WAEZJK2Y.mjs → id-5OCPPZLO.mjs} +2 -1
  30. package/dist/id-5OCPPZLO.mjs.map +1 -0
  31. package/dist/index.d.mts +102 -106
  32. package/dist/index.mjs +1814 -1450
  33. package/dist/index.mjs.map +1 -1
  34. package/dist/{it-VDNDMZPU.mjs → it-PNBBZSM2.mjs} +2 -1
  35. package/dist/it-PNBBZSM2.mjs.map +1 -0
  36. package/dist/{ja-5PEH56J5.mjs → ja-LDD7R3TJ.mjs} +2 -1
  37. package/dist/ja-LDD7R3TJ.mjs.map +1 -0
  38. package/dist/{ko-JYPL3WVA.mjs → ko-F47ZDEY3.mjs} +2 -1
  39. package/dist/ko-F47ZDEY3.mjs.map +1 -0
  40. package/dist/{ms-5PZVW76T.mjs → ms-Z7LMXJWL.mjs} +2 -1
  41. package/dist/ms-Z7LMXJWL.mjs.map +1 -0
  42. package/dist/{nl-YXES36KM.mjs → nl-6SJFBPJ3.mjs} +2 -1
  43. package/dist/nl-6SJFBPJ3.mjs.map +1 -0
  44. package/dist/{no-XRA2UCQD.mjs → no-YXPBPSGF.mjs} +2 -1
  45. package/dist/no-YXPBPSGF.mjs.map +1 -0
  46. package/dist/{pl-WH6LJA5G.mjs → pl-P4AZ2QME.mjs} +2 -1
  47. package/dist/pl-P4AZ2QME.mjs.map +1 -0
  48. package/dist/{pt-7GAG57BM.mjs → pt-LHWUS6U6.mjs} +2 -1
  49. package/dist/pt-LHWUS6U6.mjs.map +1 -0
  50. package/dist/{ro-BTDDRB7N.mjs → ro-EA5J2ZON.mjs} +2 -1
  51. package/dist/ro-EA5J2ZON.mjs.map +1 -0
  52. package/dist/{sv-7V5C2IT4.mjs → sv-DATBS3UQ.mjs} +2 -1
  53. package/dist/sv-DATBS3UQ.mjs.map +1 -0
  54. package/dist/test-utils.mjs +2 -2
  55. package/dist/{th-LPKYLBX5.mjs → th-WTFJRWPT.mjs} +2 -1
  56. package/dist/th-WTFJRWPT.mjs.map +1 -0
  57. package/dist/{tr-DU4RQL4M.mjs → tr-IKO3RXOX.mjs} +2 -1
  58. package/dist/tr-IKO3RXOX.mjs.map +1 -0
  59. package/dist/{uk-36UHTDDI.mjs → uk-CF6CTTRK.mjs} +2 -1
  60. package/dist/uk-CF6CTTRK.mjs.map +1 -0
  61. package/dist/{vi-GDHOUZDH.mjs → vi-AJLTXPZQ.mjs} +2 -1
  62. package/dist/vi-AJLTXPZQ.mjs.map +1 -0
  63. package/dist/{zh-TYUID4XZ.mjs → zh-U3ORHHYH.mjs} +2 -1
  64. package/dist/zh-U3ORHHYH.mjs.map +1 -0
  65. package/package.json +6 -2
  66. package/src/components/resource/AnnotateView.tsx +0 -4
  67. package/src/components/resource/AnnotationHistory.tsx +12 -13
  68. package/src/components/resource/BrowseView.tsx +8 -16
  69. package/src/components/resource/HistoryEvent.tsx +3 -4
  70. package/src/components/resource/ResourceViewer.tsx +174 -201
  71. package/src/components/resource/event-formatting.ts +316 -0
  72. package/src/components/resource/panels/AssessmentPanel.tsx +37 -9
  73. package/src/components/resource/panels/CollaborationPanel.tsx +20 -13
  74. package/src/components/resource/panels/CommentsPanel.tsx +38 -9
  75. package/src/components/resource/panels/ReferencesPanel.tsx +39 -14
  76. package/src/components/resource/panels/StatisticsPanel.tsx +9 -19
  77. package/src/components/resource/panels/TaggingPanel.tsx +27 -0
  78. package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +28 -21
  79. package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +547 -0
  80. package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +10 -0
  81. package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +10 -0
  82. package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +564 -0
  83. package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +8 -15
  84. package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +13 -6
  85. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +147 -78
  86. package/src/styles/motivations/motivation-assessment.css +28 -0
  87. package/src/styles/patterns/panel-helpers.css +26 -0
  88. package/translations/ar.json +1 -0
  89. package/translations/bn.json +1 -0
  90. package/translations/cs.json +1 -0
  91. package/translations/da.json +1 -0
  92. package/translations/de.json +1 -0
  93. package/translations/el.json +1 -0
  94. package/translations/en.json +1 -0
  95. package/translations/es.json +1 -0
  96. package/translations/fa.json +1 -0
  97. package/translations/fi.json +1 -0
  98. package/translations/fr.json +1 -0
  99. package/translations/he.json +1 -0
  100. package/translations/hi.json +1 -0
  101. package/translations/id.json +1 -0
  102. package/translations/it.json +1 -0
  103. package/translations/ja.json +1 -0
  104. package/translations/ko.json +1 -0
  105. package/translations/ms.json +1 -0
  106. package/translations/nl.json +1 -0
  107. package/translations/no.json +1 -0
  108. package/translations/pl.json +1 -0
  109. package/translations/pt.json +1 -0
  110. package/translations/ro.json +1 -0
  111. package/translations/sv.json +1 -0
  112. package/translations/th.json +1 -0
  113. package/translations/tr.json +1 -0
  114. package/translations/uk.json +1 -0
  115. package/translations/vi.json +1 -0
  116. package/translations/zh.json +1 -0
  117. package/dist/ar-RNNSPLQB.mjs.map +0 -1
  118. package/dist/bn-S2CDL7EC.mjs.map +0 -1
  119. package/dist/chunk-UDX2Q35T.mjs.map +0 -1
  120. package/dist/cs-RSV675WU.mjs.map +0 -1
  121. package/dist/da-CHXNPWJC.mjs.map +0 -1
  122. package/dist/de-KPEZ53D4.mjs.map +0 -1
  123. package/dist/el-MW2BME5T.mjs.map +0 -1
  124. package/dist/es-HQ24NYS3.mjs.map +0 -1
  125. package/dist/fa-W34LRLHG.mjs.map +0 -1
  126. package/dist/fi-3U44IGOA.mjs.map +0 -1
  127. package/dist/fr-N7DKX6NN.mjs.map +0 -1
  128. package/dist/he-CS4WRXN3.mjs.map +0 -1
  129. package/dist/hi-GJDY46KA.mjs.map +0 -1
  130. package/dist/id-WAEZJK2Y.mjs.map +0 -1
  131. package/dist/it-VDNDMZPU.mjs.map +0 -1
  132. package/dist/ja-5PEH56J5.mjs.map +0 -1
  133. package/dist/ko-JYPL3WVA.mjs.map +0 -1
  134. package/dist/ms-5PZVW76T.mjs.map +0 -1
  135. package/dist/nl-YXES36KM.mjs.map +0 -1
  136. package/dist/no-XRA2UCQD.mjs.map +0 -1
  137. package/dist/pl-WH6LJA5G.mjs.map +0 -1
  138. package/dist/pt-7GAG57BM.mjs.map +0 -1
  139. package/dist/ro-BTDDRB7N.mjs.map +0 -1
  140. package/dist/sv-7V5C2IT4.mjs.map +0 -1
  141. package/dist/th-LPKYLBX5.mjs.map +0 -1
  142. package/dist/tr-DU4RQL4M.mjs.map +0 -1
  143. package/dist/uk-36UHTDDI.mjs.map +0 -1
  144. package/dist/vi-GDHOUZDH.mjs.map +0 -1
  145. package/dist/zh-TYUID4XZ.mjs.map +0 -1
  146. /package/dist/{chunk-35LLVRFK.mjs.map → chunk-JZIO2A3B.mjs.map} +0 -0
  147. /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, useMemo, useCallback } from 'react';
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
- // Memoized filtered documents
85
- const filteredResources = useMemo(() => {
86
- // If we have search results, show them; otherwise show recent
87
- // This ensures we show recent docs even when search returns nothing
88
- const baseDocuments = hasSearchResults
89
- ? searchDocuments
90
- : recentDocuments;
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
- useQueryClient: () => ({
16
- invalidateQueries: vi.fn(),
17
- setQueryData: vi.fn(),
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