@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.
Files changed (53) hide show
  1. package/dist/{PdfAnnotationCanvas.client-KWQ7XDWT.js → PdfAnnotationCanvas.client-75GY2EDR.js} +12 -6
  2. package/dist/PdfAnnotationCanvas.client-75GY2EDR.js.map +1 -0
  3. package/dist/{chunk-O2MD7TGE.js → chunk-VXASXU4K.js} +10 -2
  4. package/dist/chunk-VXASXU4K.js.map +1 -0
  5. package/dist/index.d.ts +54 -16
  6. package/dist/index.js +395 -343
  7. package/dist/index.js.map +1 -1
  8. package/dist/test-utils.d.ts +5 -1
  9. package/dist/test-utils.js +3 -2
  10. package/dist/test-utils.js.map +1 -1
  11. package/package.json +5 -5
  12. package/src/components/image-annotation/AnnotationOverlay.tsx +6 -6
  13. package/src/components/image-annotation/SvgDrawingCanvas.tsx +12 -4
  14. package/src/components/image-annotation/__tests__/AnnotationOverlay.click.test.tsx +137 -0
  15. package/src/components/image-annotation/__tests__/SvgDrawingCanvas.click.test.tsx +108 -0
  16. package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +12 -4
  17. package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +137 -1
  18. package/src/components/resource/BrowseView.tsx +14 -6
  19. package/src/components/resource/ResourceViewer.tsx +6 -4
  20. package/src/components/resource/__tests__/BrowseView.test.tsx +98 -31
  21. package/src/components/resource/__tests__/ResourceViewer.embeddable.test.tsx +40 -1
  22. package/src/components/resource/__tests__/browse-renderers.dispatch.test.tsx +113 -0
  23. package/src/components/resource/browse-renderers.tsx +26 -4
  24. package/src/components/resource/panels/AssessmentEntry.tsx +5 -4
  25. package/src/components/resource/panels/AssessmentPanel.tsx +8 -5
  26. package/src/components/resource/panels/AssistSection.tsx +4 -3
  27. package/src/components/resource/panels/CommentEntry.tsx +5 -4
  28. package/src/components/resource/panels/CommentsPanel.tsx +8 -5
  29. package/src/components/resource/panels/HighlightEntry.tsx +5 -4
  30. package/src/components/resource/panels/HighlightPanel.tsx +8 -5
  31. package/src/components/resource/panels/ReferenceEntry.tsx +9 -10
  32. package/src/components/resource/panels/ReferencesPanel.tsx +11 -6
  33. package/src/components/resource/panels/ResourceInfoPanel.tsx +4 -3
  34. package/src/components/resource/panels/TagEntry.tsx +5 -3
  35. package/src/components/resource/panels/TaggingPanel.tsx +7 -4
  36. package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +10 -0
  37. package/src/components/resource/panels/__tests__/AssessmentEntry.test.tsx +28 -20
  38. package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +38 -28
  39. package/src/components/resource/panels/__tests__/AssistSection.test.tsx +46 -17
  40. package/src/components/resource/panels/__tests__/CommentEntry.test.tsx +65 -57
  41. package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +53 -24
  42. package/src/components/resource/panels/__tests__/HighlightEntry.test.tsx +27 -17
  43. package/src/components/resource/panels/__tests__/HighlightPanel.annotationProgress.test.tsx +23 -17
  44. package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +48 -77
  45. package/src/components/resource/panels/__tests__/ReferencesPanel.headless.test.tsx +87 -0
  46. package/src/components/resource/panels/__tests__/ReferencesPanel.observable-flow.test.tsx +15 -9
  47. package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +77 -59
  48. package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +11 -4
  49. package/src/components/resource/panels/__tests__/TagEntry.test.tsx +33 -19
  50. package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +15 -7
  51. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +3 -0
  52. package/dist/PdfAnnotationCanvas.client-KWQ7XDWT.js.map +0 -1
  53. 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 { renderWithProviders } from '../../../../test-utils';
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
- const defaultProps = {
83
- reference: createMockReference(),
84
- isFocused: false,
85
- routes: mockRoutes,
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
- renderWithProviders(<ReferenceEntry {...defaultProps} />);
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
- renderWithProviders(<ReferenceEntry {...defaultProps} />);
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 } = renderWithProviders(<ReferenceEntry {...defaultProps} />);
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 } = renderWithProviders(<ReferenceEntry {...defaultProps} />);
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
- renderWithProviders(<ReferenceEntry {...defaultProps} />);
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
- renderWithProviders(<ReferenceEntry {...defaultProps} />);
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
- renderWithProviders(<ReferenceEntry {...defaultProps} />);
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
- renderWithProviders(<ReferenceEntry {...defaultProps} reference={enrichedRef as Annotation} />);
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
- renderWithProviders(<ReferenceEntry {...defaultProps} />);
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 } = renderWithProviders(<ReferenceEntry {...defaultProps} />);
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 } = renderWithProviders(
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 } = renderWithProviders(
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 } = renderWithProviders(
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 } = renderWithProviders(
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, eventBus } = renderWithProviders(
241
- <ReferenceEntry {...defaultProps} />,
242
- { returnEventBus: true }
243
- );
230
+ const { container } = renderEntry();
244
231
 
245
- const subscription = eventBus!.get('browse:click').subscribe(clickHandler);
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 navigate on 🔗 icon click', async () => {
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 } = renderWithProviders(<ReferenceEntry {...defaultProps} />);
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(mockRoutes.resourceDetail).toHaveBeenCalledWith('linked-doc');
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 } = renderWithProviders(<ReferenceEntry {...defaultProps} />);
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 } = renderWithProviders(
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 } = renderWithProviders(
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 } = renderWithProviders(
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, eventBus } = renderWithProviders(
337
- <ReferenceEntry {...defaultProps} annotateMode={true} />,
338
- { returnEventBus: true }
339
- );
317
+ const { container } = renderEntry({ annotateMode: true });
340
318
 
341
- const subscription = eventBus!.get('bind:initiate').subscribe(initiateHandler);
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 } = renderWithProviders(
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 } = renderWithProviders(
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, eventBus } = renderWithProviders(
383
- <ReferenceEntry {...defaultProps} annotateMode={false} />,
384
- { returnEventBus: true }
385
- );
356
+ const { container } = renderEntry({ annotateMode: false });
386
357
 
387
- const subscription = eventBus!.get('bind:initiate').subscribe(initiateHandler);
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 } = renderWithProviders(<ReferenceEntry {...defaultProps} />);
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$ }: { source$: BehaviorSubject<string[]> }) {
70
+ function ObservableHarness({ source$, session }: { source$: BehaviorSubject<string[]>; session: SemiontSession | null }) {
70
71
  const entityTypes = useObservable(source$) ?? [];
71
72
  return (
72
- <ReferencesPanel resourceId="res-1"
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
- const renderWithBus = (ui: React.ReactElement) => {
89
- const { SemiontWrapper } = createTestSemiontWrapper();
90
- return render(<SemiontWrapper>{ui}</SemiontWrapper>);
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.