@semiont/react-ui 0.5.11 → 0.5.13
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/{PdfAnnotationCanvas.client-KWQ7XDWT.js → PdfAnnotationCanvas.client-75GY2EDR.js} +12 -6
- package/dist/PdfAnnotationCanvas.client-75GY2EDR.js.map +1 -0
- package/dist/{chunk-O2MD7TGE.js → chunk-VXASXU4K.js} +10 -2
- package/dist/chunk-VXASXU4K.js.map +1 -0
- package/dist/index.d.ts +54 -16
- package/dist/index.js +395 -343
- package/dist/index.js.map +1 -1
- package/dist/test-utils.d.ts +5 -1
- package/dist/test-utils.js +3 -2
- package/dist/test-utils.js.map +1 -1
- package/package.json +5 -5
- package/src/components/image-annotation/AnnotationOverlay.tsx +6 -6
- package/src/components/image-annotation/SvgDrawingCanvas.tsx +12 -4
- package/src/components/image-annotation/__tests__/AnnotationOverlay.click.test.tsx +137 -0
- package/src/components/image-annotation/__tests__/SvgDrawingCanvas.click.test.tsx +108 -0
- package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +12 -4
- package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +137 -1
- package/src/components/resource/BrowseView.tsx +14 -6
- package/src/components/resource/ResourceViewer.tsx +6 -4
- package/src/components/resource/__tests__/BrowseView.test.tsx +98 -31
- package/src/components/resource/__tests__/ResourceViewer.embeddable.test.tsx +40 -1
- package/src/components/resource/__tests__/browse-renderers.dispatch.test.tsx +113 -0
- package/src/components/resource/browse-renderers.tsx +26 -4
- package/src/components/resource/panels/AssessmentEntry.tsx +5 -4
- package/src/components/resource/panels/AssessmentPanel.tsx +8 -5
- package/src/components/resource/panels/AssistSection.tsx +4 -3
- package/src/components/resource/panels/CommentEntry.tsx +5 -4
- package/src/components/resource/panels/CommentsPanel.tsx +8 -5
- package/src/components/resource/panels/HighlightEntry.tsx +5 -4
- package/src/components/resource/panels/HighlightPanel.tsx +8 -5
- package/src/components/resource/panels/ReferenceEntry.tsx +9 -10
- package/src/components/resource/panels/ReferencesPanel.tsx +11 -6
- package/src/components/resource/panels/ResourceInfoPanel.tsx +4 -3
- package/src/components/resource/panels/TagEntry.tsx +5 -3
- package/src/components/resource/panels/TaggingPanel.tsx +7 -4
- package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +10 -0
- package/src/components/resource/panels/__tests__/AssessmentEntry.test.tsx +28 -20
- package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +38 -28
- package/src/components/resource/panels/__tests__/AssistSection.test.tsx +46 -17
- package/src/components/resource/panels/__tests__/CommentEntry.test.tsx +65 -57
- package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +53 -24
- package/src/components/resource/panels/__tests__/HighlightEntry.test.tsx +27 -17
- package/src/components/resource/panels/__tests__/HighlightPanel.annotationProgress.test.tsx +23 -17
- package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +48 -77
- package/src/components/resource/panels/__tests__/ReferencesPanel.headless.test.tsx +87 -0
- package/src/components/resource/panels/__tests__/ReferencesPanel.observable-flow.test.tsx +15 -9
- package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +77 -59
- package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +11 -4
- package/src/components/resource/panels/__tests__/TagEntry.test.tsx +33 -19
- package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +15 -7
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +3 -0
- package/dist/PdfAnnotationCanvas.client-KWQ7XDWT.js.map +0 -1
- package/dist/chunk-O2MD7TGE.js.map +0 -1
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
2
|
import { screen } from '@testing-library/react';
|
|
3
3
|
import '@testing-library/jest-dom';
|
|
4
|
-
import {
|
|
4
|
+
import type { ComponentProps } from 'react';
|
|
5
|
+
import { renderWithProviders, createTestSemiontWrapper } from '../../../../test-utils';
|
|
5
6
|
import userEvent from '@testing-library/user-event';
|
|
6
7
|
import { BindNamespace } from '@semiont/sdk';
|
|
7
|
-
import type { RouteBuilder } from '../../../../contexts/RoutingContext';
|
|
8
8
|
|
|
9
9
|
import type { Annotation, AnnotationId } from '@semiont/core';
|
|
10
10
|
|
|
@@ -16,7 +16,6 @@ const mockGetFragmentSelector = vi.fn();
|
|
|
16
16
|
const mockGetSvgSelector = vi.fn();
|
|
17
17
|
const mockGetTargetSelector = vi.fn();
|
|
18
18
|
const mockGetEntityTypes = vi.fn();
|
|
19
|
-
const mockNavigate = vi.fn();
|
|
20
19
|
const mockHoverProps = { onMouseEnter: vi.fn(), onMouseLeave: vi.fn() };
|
|
21
20
|
|
|
22
21
|
vi.mock('@semiont/core', async () => {
|
|
@@ -40,10 +39,6 @@ vi.mock('../../../../lib/resource-utils', () => ({
|
|
|
40
39
|
getResourceIcon: vi.fn(() => '📄'),
|
|
41
40
|
}));
|
|
42
41
|
|
|
43
|
-
vi.mock('../../../../hooks/useObservableBrowse', () => ({
|
|
44
|
-
useObservableExternalNavigation: () => mockNavigate,
|
|
45
|
-
}));
|
|
46
|
-
|
|
47
42
|
vi.mock('../../../../hooks/useHoverEmitter', () => ({
|
|
48
43
|
useHoverEmitter: () => mockHoverProps,
|
|
49
44
|
}));
|
|
@@ -71,22 +66,25 @@ const createMockReference = (overrides?: Partial<Annotation>): Annotation => ({
|
|
|
71
66
|
...overrides,
|
|
72
67
|
});
|
|
73
68
|
|
|
74
|
-
const mockRoutes: RouteBuilder = {
|
|
75
|
-
resourceDetail: vi.fn((id: string) => `/resources/${id}`),
|
|
76
|
-
userProfile: vi.fn((id: string) => `/users/${id}`),
|
|
77
|
-
search: vi.fn((query: string) => `/search?q=${query}`),
|
|
78
|
-
home: vi.fn(() => '/'),
|
|
79
|
-
};
|
|
80
|
-
|
|
81
69
|
describe('ReferenceEntry', () => {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
70
|
+
// Provider-free component: the session prop is the only session it sees.
|
|
71
|
+
// Created per-test — test-utils disposes registered clients in afterEach.
|
|
72
|
+
let session: ReturnType<typeof createTestSemiontWrapper>['session'];
|
|
73
|
+
let eventBus: ReturnType<typeof createTestSemiontWrapper>['eventBus'];
|
|
74
|
+
|
|
75
|
+
const renderEntry = (props: Partial<ComponentProps<typeof ReferenceEntry>> = {}) =>
|
|
76
|
+
renderWithProviders(
|
|
77
|
+
<ReferenceEntry
|
|
78
|
+
session={session}
|
|
79
|
+
reference={createMockReference()}
|
|
80
|
+
isFocused={false}
|
|
81
|
+
{...props}
|
|
82
|
+
/>,
|
|
83
|
+
);
|
|
87
84
|
|
|
88
85
|
beforeEach(() => {
|
|
89
86
|
vi.clearAllMocks();
|
|
87
|
+
({ session, eventBus } = createTestSemiontWrapper());
|
|
90
88
|
mockGetAnnotationExactText.mockReturnValue('referenced text');
|
|
91
89
|
mockIsBodyResolved.mockReturnValue(false);
|
|
92
90
|
mockGetBodySource.mockReturnValue(null);
|
|
@@ -98,7 +96,7 @@ describe('ReferenceEntry', () => {
|
|
|
98
96
|
|
|
99
97
|
describe('Rendering', () => {
|
|
100
98
|
it('should render the selected text in quotes', () => {
|
|
101
|
-
|
|
99
|
+
renderEntry();
|
|
102
100
|
|
|
103
101
|
expect(screen.getByText(/referenced text/)).toBeInTheDocument();
|
|
104
102
|
});
|
|
@@ -107,7 +105,7 @@ describe('ReferenceEntry', () => {
|
|
|
107
105
|
const longText = 'A'.repeat(150);
|
|
108
106
|
mockGetAnnotationExactText.mockReturnValue(longText);
|
|
109
107
|
|
|
110
|
-
|
|
108
|
+
renderEntry();
|
|
111
109
|
|
|
112
110
|
expect(screen.getByText(new RegExp(`"${'A'.repeat(100)}`))).toBeInTheDocument();
|
|
113
111
|
expect(screen.getByText(/\.\.\./)).toBeInTheDocument();
|
|
@@ -116,7 +114,7 @@ describe('ReferenceEntry', () => {
|
|
|
116
114
|
it('should show stub icon when reference is not resolved', () => {
|
|
117
115
|
mockIsBodyResolved.mockReturnValue(false);
|
|
118
116
|
|
|
119
|
-
const { container } =
|
|
117
|
+
const { container } = renderEntry();
|
|
120
118
|
|
|
121
119
|
const icon = container.querySelector('.semiont-reference-icon');
|
|
122
120
|
expect(icon).toBeInTheDocument();
|
|
@@ -127,7 +125,7 @@ describe('ReferenceEntry', () => {
|
|
|
127
125
|
mockIsBodyResolved.mockReturnValue(true);
|
|
128
126
|
mockGetBodySource.mockReturnValue('linked-doc');
|
|
129
127
|
|
|
130
|
-
const { container } =
|
|
128
|
+
const { container } = renderEntry();
|
|
131
129
|
|
|
132
130
|
const icon = container.querySelector('.semiont-reference-icon');
|
|
133
131
|
expect(icon).toBeInTheDocument();
|
|
@@ -137,7 +135,7 @@ describe('ReferenceEntry', () => {
|
|
|
137
135
|
it('should show annotation type when no selected text', () => {
|
|
138
136
|
mockGetAnnotationExactText.mockReturnValue('');
|
|
139
137
|
|
|
140
|
-
|
|
138
|
+
renderEntry();
|
|
141
139
|
|
|
142
140
|
expect(screen.getByText('Annotation')).toBeInTheDocument();
|
|
143
141
|
});
|
|
@@ -146,7 +144,7 @@ describe('ReferenceEntry', () => {
|
|
|
146
144
|
mockGetAnnotationExactText.mockReturnValue('');
|
|
147
145
|
mockGetFragmentSelector.mockReturnValue({ type: 'FragmentSelector', value: 'xywh=0,0,100,100' });
|
|
148
146
|
|
|
149
|
-
|
|
147
|
+
renderEntry();
|
|
150
148
|
|
|
151
149
|
expect(screen.getByText('Fragment annotation')).toBeInTheDocument();
|
|
152
150
|
});
|
|
@@ -155,7 +153,7 @@ describe('ReferenceEntry', () => {
|
|
|
155
153
|
mockGetAnnotationExactText.mockReturnValue('');
|
|
156
154
|
mockGetSvgSelector.mockReturnValue({ type: 'SvgSelector', value: '<svg/>' });
|
|
157
155
|
|
|
158
|
-
|
|
156
|
+
renderEntry();
|
|
159
157
|
|
|
160
158
|
expect(screen.getByText('Image annotation')).toBeInTheDocument();
|
|
161
159
|
});
|
|
@@ -170,7 +168,7 @@ describe('ReferenceEntry', () => {
|
|
|
170
168
|
_resolvedDocumentMediaType: 'text/plain',
|
|
171
169
|
};
|
|
172
170
|
|
|
173
|
-
|
|
171
|
+
renderEntry({ reference: enrichedRef as Annotation });
|
|
174
172
|
|
|
175
173
|
expect(screen.getByText(/My Linked Document/)).toBeInTheDocument();
|
|
176
174
|
});
|
|
@@ -180,7 +178,7 @@ describe('ReferenceEntry', () => {
|
|
|
180
178
|
it('should render entity type badges', () => {
|
|
181
179
|
mockGetEntityTypes.mockReturnValue(['Person', 'Organization']);
|
|
182
180
|
|
|
183
|
-
|
|
181
|
+
renderEntry();
|
|
184
182
|
|
|
185
183
|
expect(screen.getByText('Person')).toBeInTheDocument();
|
|
186
184
|
expect(screen.getByText('Organization')).toBeInTheDocument();
|
|
@@ -189,7 +187,7 @@ describe('ReferenceEntry', () => {
|
|
|
189
187
|
it('should not render entity type section when empty', () => {
|
|
190
188
|
mockGetEntityTypes.mockReturnValue([]);
|
|
191
189
|
|
|
192
|
-
const { container } =
|
|
190
|
+
const { container } = renderEntry();
|
|
193
191
|
|
|
194
192
|
expect(container.querySelector('.semiont-annotation-entry__tags')).not.toBeInTheDocument();
|
|
195
193
|
});
|
|
@@ -197,36 +195,28 @@ describe('ReferenceEntry', () => {
|
|
|
197
195
|
|
|
198
196
|
describe('Focus and hover state', () => {
|
|
199
197
|
it('should set data-focused to true when focused', () => {
|
|
200
|
-
const { container } =
|
|
201
|
-
<ReferenceEntry {...defaultProps} isFocused={true} />
|
|
202
|
-
);
|
|
198
|
+
const { container } = renderEntry({ isFocused: true });
|
|
203
199
|
|
|
204
200
|
const entry = container.firstChild as HTMLElement;
|
|
205
201
|
expect(entry).toHaveAttribute('data-focused', 'true');
|
|
206
202
|
});
|
|
207
203
|
|
|
208
204
|
it('should set data-focused to false when not focused', () => {
|
|
209
|
-
const { container } =
|
|
210
|
-
<ReferenceEntry {...defaultProps} isFocused={false} />
|
|
211
|
-
);
|
|
205
|
+
const { container } = renderEntry({ isFocused: false });
|
|
212
206
|
|
|
213
207
|
const entry = container.firstChild as HTMLElement;
|
|
214
208
|
expect(entry).toHaveAttribute('data-focused', 'false');
|
|
215
209
|
});
|
|
216
210
|
|
|
217
211
|
it('should apply pulse class when isHovered is true', () => {
|
|
218
|
-
const { container } =
|
|
219
|
-
<ReferenceEntry {...defaultProps} isHovered={true} />
|
|
220
|
-
);
|
|
212
|
+
const { container } = renderEntry({ isHovered: true });
|
|
221
213
|
|
|
222
214
|
const entry = container.firstChild as HTMLElement;
|
|
223
215
|
expect(entry).toHaveClass('semiont-annotation-pulse');
|
|
224
216
|
});
|
|
225
217
|
|
|
226
218
|
it('should not apply pulse class when isHovered is false', () => {
|
|
227
|
-
const { container } =
|
|
228
|
-
<ReferenceEntry {...defaultProps} isHovered={false} />
|
|
229
|
-
);
|
|
219
|
+
const { container } = renderEntry({ isHovered: false });
|
|
230
220
|
|
|
231
221
|
const entry = container.firstChild as HTMLElement;
|
|
232
222
|
expect(entry).not.toHaveClass('semiont-annotation-pulse');
|
|
@@ -237,12 +227,9 @@ describe('ReferenceEntry', () => {
|
|
|
237
227
|
it('should emit browse:click on click', async () => {
|
|
238
228
|
const clickHandler = vi.fn();
|
|
239
229
|
|
|
240
|
-
const { container
|
|
241
|
-
<ReferenceEntry {...defaultProps} />,
|
|
242
|
-
{ returnEventBus: true }
|
|
243
|
-
);
|
|
230
|
+
const { container } = renderEntry();
|
|
244
231
|
|
|
245
|
-
const subscription = eventBus
|
|
232
|
+
const subscription = eventBus.get('browse:click').subscribe(clickHandler);
|
|
246
233
|
|
|
247
234
|
const entry = container.firstChild as HTMLElement;
|
|
248
235
|
await userEvent.click(entry);
|
|
@@ -257,24 +244,24 @@ describe('ReferenceEntry', () => {
|
|
|
257
244
|
});
|
|
258
245
|
|
|
259
246
|
describe('Status icon — resolved reference', () => {
|
|
260
|
-
it('should
|
|
247
|
+
it('should call onOpenResource on 🔗 icon click', async () => {
|
|
261
248
|
mockIsBodyResolved.mockReturnValue(true);
|
|
262
249
|
mockGetBodySource.mockReturnValue('linked-doc');
|
|
250
|
+
const onOpenResource = vi.fn();
|
|
263
251
|
|
|
264
|
-
const { container } =
|
|
252
|
+
const { container } = renderEntry({ onOpenResource });
|
|
265
253
|
|
|
266
254
|
const icon = container.querySelector('.semiont-reference-icon')!;
|
|
267
255
|
await userEvent.click(icon);
|
|
268
256
|
|
|
269
|
-
expect(
|
|
270
|
-
expect(mockNavigate).toHaveBeenCalledWith('/resources/linked-doc', { resourceId: 'linked-doc' });
|
|
257
|
+
expect(onOpenResource).toHaveBeenCalledWith('linked-doc');
|
|
271
258
|
});
|
|
272
259
|
|
|
273
260
|
it('should have clickable class when resolved', () => {
|
|
274
261
|
mockIsBodyResolved.mockReturnValue(true);
|
|
275
262
|
mockGetBodySource.mockReturnValue('linked-doc');
|
|
276
263
|
|
|
277
|
-
const { container } =
|
|
264
|
+
const { container } = renderEntry();
|
|
278
265
|
|
|
279
266
|
const icon = container.querySelector('.semiont-reference-icon');
|
|
280
267
|
expect(icon).toHaveClass('semiont-reference-icon--clickable');
|
|
@@ -284,9 +271,7 @@ describe('ReferenceEntry', () => {
|
|
|
284
271
|
mockIsBodyResolved.mockReturnValue(true);
|
|
285
272
|
mockGetBodySource.mockReturnValue('linked-doc');
|
|
286
273
|
|
|
287
|
-
const { container } =
|
|
288
|
-
<ReferenceEntry {...defaultProps} annotateMode={true} />
|
|
289
|
-
);
|
|
274
|
+
const { container } = renderEntry({ annotateMode: true });
|
|
290
275
|
|
|
291
276
|
const unlinkButton = container.querySelector('.semiont-reference-unlink');
|
|
292
277
|
expect(unlinkButton).toBeInTheDocument();
|
|
@@ -296,9 +281,7 @@ describe('ReferenceEntry', () => {
|
|
|
296
281
|
mockIsBodyResolved.mockReturnValue(true);
|
|
297
282
|
mockGetBodySource.mockReturnValue('linked-doc');
|
|
298
283
|
|
|
299
|
-
const { container } =
|
|
300
|
-
<ReferenceEntry {...defaultProps} annotateMode={false} />
|
|
301
|
-
);
|
|
284
|
+
const { container } = renderEntry({ annotateMode: false });
|
|
302
285
|
|
|
303
286
|
const unlinkButton = container.querySelector('.semiont-reference-unlink');
|
|
304
287
|
expect(unlinkButton).not.toBeInTheDocument();
|
|
@@ -310,9 +293,7 @@ describe('ReferenceEntry', () => {
|
|
|
310
293
|
|
|
311
294
|
const bindSpy = vi.spyOn(BindNamespace.prototype, 'body').mockResolvedValue(undefined);
|
|
312
295
|
|
|
313
|
-
const { container } =
|
|
314
|
-
<ReferenceEntry {...defaultProps} annotateMode={true} />,
|
|
315
|
-
);
|
|
296
|
+
const { container } = renderEntry({ annotateMode: true });
|
|
316
297
|
|
|
317
298
|
const unlinkButton = container.querySelector('.semiont-reference-unlink')!;
|
|
318
299
|
await userEvent.click(unlinkButton);
|
|
@@ -333,12 +314,9 @@ describe('ReferenceEntry', () => {
|
|
|
333
314
|
mockGetEntityTypes.mockReturnValue(['Person']);
|
|
334
315
|
const initiateHandler = vi.fn();
|
|
335
316
|
|
|
336
|
-
const { container
|
|
337
|
-
<ReferenceEntry {...defaultProps} annotateMode={true} />,
|
|
338
|
-
{ returnEventBus: true }
|
|
339
|
-
);
|
|
317
|
+
const { container } = renderEntry({ annotateMode: true });
|
|
340
318
|
|
|
341
|
-
const subscription = eventBus
|
|
319
|
+
const subscription = eventBus.get('bind:initiate').subscribe(initiateHandler);
|
|
342
320
|
|
|
343
321
|
const icon = container.querySelector('.semiont-reference-icon')!;
|
|
344
322
|
await userEvent.click(icon);
|
|
@@ -356,9 +334,7 @@ describe('ReferenceEntry', () => {
|
|
|
356
334
|
it('should have clickable class in annotate mode', () => {
|
|
357
335
|
mockIsBodyResolved.mockReturnValue(false);
|
|
358
336
|
|
|
359
|
-
const { container } =
|
|
360
|
-
<ReferenceEntry {...defaultProps} annotateMode={true} />
|
|
361
|
-
);
|
|
337
|
+
const { container } = renderEntry({ annotateMode: true });
|
|
362
338
|
|
|
363
339
|
const icon = container.querySelector('.semiont-reference-icon');
|
|
364
340
|
expect(icon).toHaveClass('semiont-reference-icon--clickable');
|
|
@@ -367,9 +343,7 @@ describe('ReferenceEntry', () => {
|
|
|
367
343
|
it('should not be clickable in browse mode', () => {
|
|
368
344
|
mockIsBodyResolved.mockReturnValue(false);
|
|
369
345
|
|
|
370
|
-
const { container } =
|
|
371
|
-
<ReferenceEntry {...defaultProps} annotateMode={false} />
|
|
372
|
-
);
|
|
346
|
+
const { container } = renderEntry({ annotateMode: false });
|
|
373
347
|
|
|
374
348
|
const icon = container.querySelector('.semiont-reference-icon');
|
|
375
349
|
expect(icon).not.toHaveClass('semiont-reference-icon--clickable');
|
|
@@ -379,12 +353,9 @@ describe('ReferenceEntry', () => {
|
|
|
379
353
|
mockIsBodyResolved.mockReturnValue(false);
|
|
380
354
|
const initiateHandler = vi.fn();
|
|
381
355
|
|
|
382
|
-
const { container
|
|
383
|
-
<ReferenceEntry {...defaultProps} annotateMode={false} />,
|
|
384
|
-
{ returnEventBus: true }
|
|
385
|
-
);
|
|
356
|
+
const { container } = renderEntry({ annotateMode: false });
|
|
386
357
|
|
|
387
|
-
const subscription = eventBus
|
|
358
|
+
const subscription = eventBus.get('bind:initiate').subscribe(initiateHandler);
|
|
388
359
|
|
|
389
360
|
const icon = container.querySelector('.semiont-reference-icon')!;
|
|
390
361
|
await userEvent.click(icon);
|
|
@@ -397,7 +368,7 @@ describe('ReferenceEntry', () => {
|
|
|
397
368
|
|
|
398
369
|
describe('data-type attribute', () => {
|
|
399
370
|
it('should have data-type="reference"', () => {
|
|
400
|
-
const { container } =
|
|
371
|
+
const { container } = renderEntry();
|
|
401
372
|
|
|
402
373
|
const entry = container.firstChild as HTMLElement;
|
|
403
374
|
expect(entry).toHaveAttribute('data-type', 'reference');
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HEADLESS-ANNOTATION-PANELS Phase 1 (B2) — keystone: the panel family mounts
|
|
3
|
+
* provider-free with a session PROP.
|
|
4
|
+
*
|
|
5
|
+
* ReferencesPanel rendered with a fake session prop and NO SemiontProvider /
|
|
6
|
+
* routing contexts (Link/routes are already props; translations fall back to
|
|
7
|
+
* bundled English): lists a reference annotation, and an entry click reaches
|
|
8
|
+
* session.client.browse.click — the same interaction the Browser gets, no
|
|
9
|
+
* provider anywhere.
|
|
10
|
+
*
|
|
11
|
+
* Started RED (the family reads useSemiont() — provider crash; no session
|
|
12
|
+
* prop) and GREEN once B2 lands.
|
|
13
|
+
*/
|
|
14
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
15
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
16
|
+
import '@testing-library/jest-dom';
|
|
17
|
+
import type { SemiontSession } from '@semiont/sdk';
|
|
18
|
+
import type { Annotation, AnnotationId } from '@semiont/core';
|
|
19
|
+
import type { RouteBuilder, LinkComponentProps } from '../../../../contexts/RoutingContext';
|
|
20
|
+
import { ReferencesPanel } from '../ReferencesPanel';
|
|
21
|
+
|
|
22
|
+
function fakeSession() {
|
|
23
|
+
const client = {
|
|
24
|
+
browse: { click: vi.fn(), tagSchemas: () => ({ subscribe: () => ({ unsubscribe: () => {} }) }) },
|
|
25
|
+
beckon: { hover: vi.fn(), sparkle: vi.fn() },
|
|
26
|
+
mark: { requestAssist: vi.fn(), delete: vi.fn() },
|
|
27
|
+
};
|
|
28
|
+
const session = {
|
|
29
|
+
client,
|
|
30
|
+
subscribe: () => () => {},
|
|
31
|
+
} as unknown as SemiontSession;
|
|
32
|
+
return { session, client };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function referenceAnnotation(): Annotation {
|
|
36
|
+
return {
|
|
37
|
+
'@context': 'http://www.w3.org/ns/anno.jsonld',
|
|
38
|
+
id: 'ref-1' as AnnotationId,
|
|
39
|
+
type: 'Annotation',
|
|
40
|
+
motivation: 'linking',
|
|
41
|
+
creator: { '@type': 'Person', name: 'user@example.com' },
|
|
42
|
+
created: '2026-07-14T00:00:00Z',
|
|
43
|
+
target: {
|
|
44
|
+
source: 'res-1',
|
|
45
|
+
selector: [
|
|
46
|
+
{ type: 'TextQuoteSelector', exact: 'linked text' },
|
|
47
|
+
{ type: 'TextPositionSelector', start: 0, end: 11 },
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
} as unknown as Annotation;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const TestLink = ({ href, children, ...rest }: LinkComponentProps) => (
|
|
54
|
+
<a href={href} {...rest}>{children}</a>
|
|
55
|
+
);
|
|
56
|
+
const testRoutes = {
|
|
57
|
+
resourceDetail: (id: string) => `/r/${id}`,
|
|
58
|
+
know: '/know',
|
|
59
|
+
} as unknown as RouteBuilder;
|
|
60
|
+
|
|
61
|
+
describe('ReferencesPanel — headless (session prop, no providers)', () => {
|
|
62
|
+
beforeEach(() => vi.clearAllMocks());
|
|
63
|
+
|
|
64
|
+
it('mounts provider-free, lists the reference, and routes an entry click through the session prop', () => {
|
|
65
|
+
const { session, client } = fakeSession();
|
|
66
|
+
|
|
67
|
+
render(
|
|
68
|
+
<ReferencesPanel
|
|
69
|
+
session={session}
|
|
70
|
+
resourceId="res-1"
|
|
71
|
+
annotations={[referenceAnnotation()]}
|
|
72
|
+
isAssisting={false}
|
|
73
|
+
progress={null}
|
|
74
|
+
pendingAnnotation={null}
|
|
75
|
+
allEntityTypes={[]}
|
|
76
|
+
Link={TestLink}
|
|
77
|
+
routes={testRoutes}
|
|
78
|
+
/>,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const entryText = screen.getByText(/linked text/);
|
|
82
|
+
expect(entryText).toBeInTheDocument();
|
|
83
|
+
|
|
84
|
+
fireEvent.click(entryText);
|
|
85
|
+
expect(client.browse.click).toHaveBeenCalledWith('ref-1', 'linking');
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -20,6 +20,7 @@ import React from 'react';
|
|
|
20
20
|
import { render, screen, act } from '@testing-library/react';
|
|
21
21
|
import '@testing-library/jest-dom';
|
|
22
22
|
import { BehaviorSubject } from 'rxjs';
|
|
23
|
+
import type { SemiontSession } from '@semiont/sdk';
|
|
23
24
|
import { ReferencesPanel } from '../ReferencesPanel';
|
|
24
25
|
import { createTestSemiontWrapper } from '../../../../test-utils';
|
|
25
26
|
import { useObservable } from '../../../../hooks/useObservable';
|
|
@@ -66,10 +67,11 @@ const mockRoutes = { resourceDetail: (id: string) => `/resource/${id}` } as any;
|
|
|
66
67
|
* same hook ResourceViewerPage uses for vm.entityTypes$) and forwards
|
|
67
68
|
* its value into ReferencesPanel as `allEntityTypes`.
|
|
68
69
|
*/
|
|
69
|
-
function ObservableHarness({ source
|
|
70
|
+
function ObservableHarness({ source$, session }: { source$: BehaviorSubject<string[]>; session: SemiontSession | null }) {
|
|
70
71
|
const entityTypes = useObservable(source$) ?? [];
|
|
71
72
|
return (
|
|
72
|
-
<ReferencesPanel
|
|
73
|
+
<ReferencesPanel session={session}
|
|
74
|
+
resourceId="res-1"
|
|
73
75
|
annotations={[]}
|
|
74
76
|
isAssisting={false}
|
|
75
77
|
progress={null}
|
|
@@ -85,15 +87,19 @@ function ObservableHarness({ source$ }: { source$: BehaviorSubject<string[]> })
|
|
|
85
87
|
);
|
|
86
88
|
}
|
|
87
89
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
90
|
+
// Callback form so the UI is built AFTER the per-call factory runs: the
|
|
91
|
+
// provider-free ReferencesPanel only sees the `session` prop, which must be
|
|
92
|
+
// the fake session from this call (test-utils disposes each created client
|
|
93
|
+
// in a module-scope afterEach, so no module-scope session either).
|
|
94
|
+
const renderWithBus = (ui: (session: SemiontSession) => React.ReactElement) => {
|
|
95
|
+
const { SemiontWrapper, session } = createTestSemiontWrapper();
|
|
96
|
+
return render(<SemiontWrapper>{ui(session)}</SemiontWrapper>);
|
|
91
97
|
};
|
|
92
98
|
|
|
93
99
|
describe('Layer 5-6 — state-unit observable → useObservable → ReferencesPanel chips', () => {
|
|
94
100
|
it('an observable seeded with [9 strings] renders 9 pending-reference chips', async () => {
|
|
95
101
|
const source$ = new BehaviorSubject<string[]>(NINE_TYPES);
|
|
96
|
-
renderWithBus(<ObservableHarness source$={source$} />);
|
|
102
|
+
renderWithBus((session) => <ObservableHarness source$={source$} session={session} />);
|
|
97
103
|
|
|
98
104
|
// Wait for useEffect in useObservable to run and commit the value.
|
|
99
105
|
const chips = await screen.findAllByRole('button', { name: (_, el) =>
|
|
@@ -107,7 +113,7 @@ describe('Layer 5-6 — state-unit observable → useObservable → ReferencesPa
|
|
|
107
113
|
// to []) first, then the 9-string array once fetch resolves. The
|
|
108
114
|
// prop chain must survive this transition.
|
|
109
115
|
const source$ = new BehaviorSubject<string[]>([]);
|
|
110
|
-
renderWithBus(<ObservableHarness source$={source$} />);
|
|
116
|
+
renderWithBus((session) => <ObservableHarness source$={source$} session={session} />);
|
|
111
117
|
|
|
112
118
|
// Initially: no chips (the gate is allEntityTypes.length > 0).
|
|
113
119
|
expect(document.querySelectorAll('.semiont-tag-selector__item').length).toBe(0);
|
|
@@ -126,7 +132,7 @@ describe('Layer 5-6 — state-unit observable → useObservable → ReferencesPa
|
|
|
126
132
|
// to concurrent SSE streams. Same data, but multiple BehaviorSubject
|
|
127
133
|
// writes. Must not clobber the render.
|
|
128
134
|
const source$ = new BehaviorSubject<string[]>([]);
|
|
129
|
-
renderWithBus(<ObservableHarness source$={source$} />);
|
|
135
|
+
renderWithBus((session) => <ObservableHarness source$={source$} session={session} />);
|
|
130
136
|
|
|
131
137
|
await act(async () => {
|
|
132
138
|
source$.next(NINE_TYPES);
|
|
@@ -143,7 +149,7 @@ describe('Layer 5-6 — state-unit observable → useObservable → ReferencesPa
|
|
|
143
149
|
// message the failing e2e saw. Guards against the test passing
|
|
144
150
|
// trivially because of a selector bug.
|
|
145
151
|
const source$ = new BehaviorSubject<string[]>([]);
|
|
146
|
-
renderWithBus(<ObservableHarness source$={source$} />);
|
|
152
|
+
renderWithBus((session) => <ObservableHarness source$={source$} session={session} />);
|
|
147
153
|
|
|
148
154
|
// There are two such text nodes in the panel (pending prompt + assist
|
|
149
155
|
// section), but both correspond to the same allEntityTypes=[] state.
|