@semiont/react-ui 0.5.10 → 0.5.12

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 (81) hide show
  1. package/README.md +1 -1
  2. package/dist/{PdfAnnotationCanvas.client-P37SXBWQ.js → PdfAnnotationCanvas.client-75GY2EDR.js} +15 -6
  3. package/dist/PdfAnnotationCanvas.client-75GY2EDR.js.map +1 -0
  4. package/dist/{chunk-5LFFAVDI.js → chunk-PFUXQPWJ.js} +96 -96
  5. package/dist/{chunk-5LFFAVDI.js.map → chunk-PFUXQPWJ.js.map} +1 -1
  6. package/dist/{chunk-O2MD7TGE.js → chunk-VXASXU4K.js} +10 -2
  7. package/dist/chunk-VXASXU4K.js.map +1 -0
  8. package/dist/index.css +150 -112
  9. package/dist/index.css.map +1 -1
  10. package/dist/index.d.ts +334 -48
  11. package/dist/index.js +2494 -2201
  12. package/dist/index.js.map +1 -1
  13. package/dist/test-utils.d.ts +5 -1
  14. package/dist/test-utils.js +10 -4
  15. package/dist/test-utils.js.map +1 -1
  16. package/package.json +7 -7
  17. package/src/components/ResourceTagsInline.tsx +94 -11
  18. package/src/components/StatusDisplay.css +36 -0
  19. package/src/components/__tests__/ResourceTagsInline.test.tsx +101 -3
  20. package/src/components/annotation/AnnotateToolbar.tsx +53 -28
  21. package/src/components/annotation/__tests__/AnnotateToolbar.display.test.tsx +76 -0
  22. package/src/components/annotation/__tests__/AnnotateToolbar.test.tsx +124 -360
  23. package/src/components/image-annotation/AnnotationOverlay.tsx +6 -6
  24. package/src/components/image-annotation/SvgDrawingCanvas.tsx +17 -5
  25. package/src/components/image-annotation/__tests__/AnnotationOverlay.click.test.tsx +137 -0
  26. package/src/components/image-annotation/__tests__/SvgDrawingCanvas.click.test.tsx +108 -0
  27. package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +17 -4
  28. package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +143 -7
  29. package/src/components/resource/AnnotateView.tsx +59 -38
  30. package/src/components/resource/BrowseView.tsx +172 -138
  31. package/src/components/resource/ResourceViewer.tsx +123 -130
  32. package/src/components/resource/__tests__/AnnotateView.embeddable.test.tsx +55 -0
  33. package/src/components/resource/__tests__/BrowseView.embeddable.test.tsx +49 -0
  34. package/src/components/resource/__tests__/BrowseView.links.test.tsx +60 -0
  35. package/src/components/resource/__tests__/BrowseView.overlay-async.test.tsx +113 -0
  36. package/src/components/resource/__tests__/BrowseView.reference-hover.test.tsx +161 -0
  37. package/src/components/resource/__tests__/BrowseView.registry.test.tsx +43 -0
  38. package/src/components/resource/__tests__/BrowseView.test.tsx +133 -53
  39. package/src/components/resource/__tests__/ResourceViewer.embeddable.test.tsx +102 -0
  40. package/src/components/resource/__tests__/ResourceViewer.mode-switch.test.tsx +12 -7
  41. package/src/components/resource/__tests__/ResourceViewer.toolbar-opt-out.test.tsx +135 -0
  42. package/src/components/resource/__tests__/ResourceViewer.toolbar-prefs.test.tsx +200 -0
  43. package/src/components/resource/__tests__/browse-renderers.dispatch.test.tsx +113 -0
  44. package/src/components/resource/__tests__/browse-renderers.test.tsx +34 -0
  45. package/src/components/resource/__tests__/inline-embedding.test.tsx +81 -0
  46. package/src/components/resource/__tests__/media-completeness.test.tsx +88 -0
  47. package/src/components/resource/browse-renderers.tsx +85 -0
  48. package/src/components/resource/panels/AssessmentEntry.tsx +5 -4
  49. package/src/components/resource/panels/AssessmentPanel.tsx +12 -5
  50. package/src/components/resource/panels/AssistSection.tsx +4 -3
  51. package/src/components/resource/panels/CommentEntry.tsx +5 -4
  52. package/src/components/resource/panels/CommentsPanel.tsx +12 -5
  53. package/src/components/resource/panels/HighlightEntry.tsx +5 -4
  54. package/src/components/resource/panels/HighlightPanel.tsx +12 -5
  55. package/src/components/resource/panels/ReferenceEntry.tsx +9 -10
  56. package/src/components/resource/panels/ReferencesPanel.tsx +15 -6
  57. package/src/components/resource/panels/ResourceInfoPanel.tsx +4 -3
  58. package/src/components/resource/panels/TagEntry.tsx +5 -3
  59. package/src/components/resource/panels/TaggingPanel.tsx +11 -4
  60. package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +15 -2
  61. package/src/components/resource/panels/__tests__/AssessmentEntry.test.tsx +28 -20
  62. package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +39 -28
  63. package/src/components/resource/panels/__tests__/AssistSection.test.tsx +46 -17
  64. package/src/components/resource/panels/__tests__/CommentEntry.test.tsx +65 -57
  65. package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +54 -24
  66. package/src/components/resource/panels/__tests__/HighlightEntry.test.tsx +27 -17
  67. package/src/components/resource/panels/__tests__/HighlightPanel.annotationProgress.test.tsx +23 -17
  68. package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +48 -77
  69. package/src/components/resource/panels/__tests__/ReferencesPanel.headless.test.tsx +87 -0
  70. package/src/components/resource/panels/__tests__/ReferencesPanel.observable-flow.test.tsx +15 -9
  71. package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +78 -59
  72. package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +11 -4
  73. package/src/components/resource/panels/__tests__/TagEntry.test.tsx +33 -19
  74. package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +16 -7
  75. package/src/components/toolbar/Toolbar.css +14 -0
  76. package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +7 -17
  77. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +43 -17
  78. package/src/features/resource-viewer/state/resource-viewer-page-state-unit.ts +2 -14
  79. package/src/styles/features/resource-viewer.css +19 -0
  80. package/dist/PdfAnnotationCanvas.client-P37SXBWQ.js.map +0 -1
  81. package/dist/chunk-O2MD7TGE.js.map +0 -1
@@ -1,8 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { useTranslations } from '../../../contexts/TranslationContext';
4
- import { useSemiont } from '../../../session/SemiontProvider';
5
- import { useObservable } from '../../../hooks/useObservable';
4
+ import type { SemiontSession } from '@semiont/sdk';
6
5
  import { formatLocaleDisplay } from '@semiont/core';
7
6
  import { resourceId as makeResourceId, type components } from '@semiont/core';
8
7
  import { renderAgentLabel } from './agent-label';
@@ -11,6 +10,8 @@ import './ResourceInfoPanel.css';
11
10
  type Agent = components['schemas']['Agent'];
12
11
 
13
12
  interface Props {
13
+ /** Session carrying the client and event bus; null renders inert. */
14
+ session: SemiontSession | null;
14
15
  resourceId: string;
15
16
  documentEntityTypes: string[];
16
17
  documentLocale?: string | undefined;
@@ -39,6 +40,7 @@ interface Props {
39
40
  * @emits mark:archive - Archive this resource
40
41
  */
41
42
  export function ResourceInfoPanel({
43
+ session,
42
44
  resourceId,
43
45
  documentEntityTypes,
44
46
  documentLocale,
@@ -54,7 +56,6 @@ export function ResourceInfoPanel({
54
56
  onGenerate,
55
57
  }: Props) {
56
58
  const t = useTranslations('ResourceInfoPanel');
57
- const session = useObservable(useSemiont().activeSession$);
58
59
 
59
60
  // Single attribution surface. `wasAttributedTo` is the canonical list
60
61
  // of responsible parties; if a producer set only `generator` we
@@ -5,12 +5,14 @@ import type { Ref } from 'react';
5
5
  import type { Annotation } from '@semiont/core';
6
6
  import { getAnnotationExactText } from '@semiont/core';
7
7
  import { getTagCategory, getTagSchemaId } from '@semiont/ontology';
8
- import { useSemiont } from '../../../session/SemiontProvider';
9
8
  import { useObservable } from '../../../hooks/useObservable';
9
+ import type { SemiontSession } from '@semiont/sdk';
10
10
  import { useHoverEmitter } from '../../../hooks/useHoverEmitter';
11
11
  import { renderAgentLabel } from './agent-label';
12
12
 
13
13
  interface TagEntryProps {
14
+ /** Session for interaction routing (browse.click, tag schemas); the panel threads it. */
15
+ session: SemiontSession | null;
14
16
  tag: Annotation;
15
17
  isFocused: boolean;
16
18
  isHovered?: boolean;
@@ -18,13 +20,13 @@ interface TagEntryProps {
18
20
  }
19
21
 
20
22
  export function TagEntry({
23
+ session,
21
24
  tag,
22
25
  isFocused,
23
26
  isHovered = false,
24
27
  ref,
25
28
  }: TagEntryProps) {
26
- const session = useObservable(useSemiont().activeSession$);
27
- const hoverProps = useHoverEmitter(tag.id);
29
+ const hoverProps = useHoverEmitter(session, tag.id);
28
30
 
29
31
  const selectedText = getAnnotationExactText(tag);
30
32
  const category = getTagCategory(tag);
@@ -2,9 +2,9 @@
2
2
 
3
3
  import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
4
4
  import { useTranslations } from '../../../contexts/TranslationContext';
5
- import { useSemiont } from '../../../session/SemiontProvider';
6
5
  import { useObservable } from '../../../hooks/useObservable';
7
- import { useEventSubscriptions } from '../../../contexts/useEventSubscription';
6
+ import type { SemiontSession } from '@semiont/sdk';
7
+ import { useSessionEventSubscriptions } from '../../../hooks/useSessionEventSubscriptions';
8
8
  import type { components, Selector } from '@semiont/core';
9
9
  import { getTextPositionSelector, getTargetSelector } from '@semiont/core';
10
10
  import { TagEntry } from './TagEntry';
@@ -39,6 +39,10 @@ function getSelectorDisplayText(selector: Selector | Selector[]): string | null
39
39
  }
40
40
 
41
41
  interface TaggingPanelProps {
42
+ /** Session carrying the client and event bus; null renders inert. */
43
+ session: SemiontSession | null;
44
+ /** The '@id' of the panel's resource — stamped as `source` on mark:submit (multi-viewer routing). */
45
+ resourceId: string;
42
46
  annotations: Annotation[];
43
47
  annotateMode?: boolean;
44
48
  isAssisting?: boolean;
@@ -62,6 +66,8 @@ interface TaggingPanelProps {
62
66
  * @subscribes browse:click - Annotation clicked. Payload: { annotationId: string }
63
67
  */
64
68
  export function TaggingPanel({
69
+ session,
70
+ resourceId,
65
71
  annotations,
66
72
  annotateMode = true,
67
73
  isAssisting = false,
@@ -74,7 +80,6 @@ export function TaggingPanel({
74
80
  sourceLanguage,
75
81
  }: TaggingPanelProps) {
76
82
  const t = useTranslations('TaggingPanel');
77
- const session = useObservable(useSemiont().activeSession$);
78
83
 
79
84
  // Subscribe to the per-KB tag-schema registry. Schemas are runtime-
80
85
  // registered by the KB at session start (see frame.addTagSchema).
@@ -126,7 +131,7 @@ export function TaggingPanel({
126
131
  setTimeout(() => setFocusedAnnotationId(null), 3000);
127
132
  }, []);
128
133
 
129
- useEventSubscriptions({
134
+ useSessionEventSubscriptions(session, {
130
135
  'browse:click': handleAnnotationClick,
131
136
  });
132
137
 
@@ -309,6 +314,7 @@ export function TaggingPanel({
309
314
  onChange={(e) => {
310
315
  if (e.target.value && pendingAnnotation) {
311
316
  session?.client.mark.submit({
317
+ source: resourceId,
312
318
  motivation: 'tagging',
313
319
  selector: pendingAnnotation.selector,
314
320
  body: [
@@ -524,6 +530,7 @@ export function TaggingPanel({
524
530
  ) : (
525
531
  sortedAnnotations.map((tag) => (
526
532
  <TagEntry
533
+ session={session}
527
534
  key={tag.id}
528
535
  tag={tag}
529
536
  isFocused={tag.id === focusedAnnotationId}
@@ -5,6 +5,7 @@ import { useTranslations } from '../../../contexts/TranslationContext';
5
5
  import type { components, Selector } from '@semiont/core';
6
6
  type JobProgress = components['schemas']['JobProgress'];
7
7
  import type { RouteBuilder, LinkComponentProps } from '../../../contexts/RoutingContext';
8
+ import type { SemiontSession } from '@semiont/sdk';
8
9
  import type { Annotator } from '../../../lib/annotation-registry';
9
10
  import { StatisticsPanel } from './StatisticsPanel';
10
11
  import { HighlightPanel } from './HighlightPanel';
@@ -37,6 +38,12 @@ const TAB_ORDER: TabKey[] = ['statistics', 'reference', 'highlight', 'assessment
37
38
  * - All operations managed via event bus (no callback props)
38
39
  */
39
40
  interface UnifiedAnnotationsPanelProps {
41
+ /** Session carrying the client and event bus; null renders inert. */
42
+ session: SemiontSession | null;
43
+
44
+ /** Host-owned navigation: called with the resolved resource id when a reference entry opens. */
45
+ onOpenResource?: (resourceId: string) => void;
46
+
40
47
  // All annotations (grouped internally by motivation)
41
48
  annotations: Annotation[];
42
49
 
@@ -59,8 +66,9 @@ interface UnifiedAnnotationsPanelProps {
59
66
  referencedBy?: any[];
60
67
  referencedByLoading?: boolean;
61
68
 
62
- // Resource context
63
- resourceId?: string;
69
+ // Resource context — threaded to every per-motivation panel, which stamps it
70
+ // as `source` on mark:submit (multi-viewer routing).
71
+ resourceId: string;
64
72
  initialTab?: TabKey;
65
73
  initialTabGeneration?: number; // Generation counter for tab switching
66
74
 
@@ -244,6 +252,8 @@ export function UnifiedAnnotationsPanel(props: UnifiedAnnotationsPanelProps) {
244
252
 
245
253
  // Common props for all annotation panels
246
254
  const commonProps = {
255
+ session: props.session,
256
+ resourceId: props.resourceId,
247
257
  annotations,
248
258
  pendingAnnotation: props.pendingAnnotation,
249
259
  isAssisting,
@@ -268,6 +278,9 @@ export function UnifiedAnnotationsPanel(props: UnifiedAnnotationsPanelProps) {
268
278
  if (activeTab === 'reference') {
269
279
  return (
270
280
  <ReferencesPanel
281
+ session={commonProps.session}
282
+ onOpenResource={props.onOpenResource}
283
+ resourceId={commonProps.resourceId}
271
284
  annotations={commonProps.annotations}
272
285
  pendingAnnotation={commonProps.pendingAnnotation}
273
286
  isAssisting={commonProps.isAssisting}
@@ -1,10 +1,11 @@
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 { renderWithProviders, createTestSemiontWrapper } from '../../../../test-utils';
5
5
  import userEvent from '@testing-library/user-event';
6
6
 
7
- import type { Annotation, AnnotationId } from '@semiont/core';
7
+ import type { Annotation, AnnotationId, EventBus } from '@semiont/core';
8
+ import type { SemiontSession } from '@semiont/sdk';
8
9
 
9
10
  // Mock @semiont/http-transport
10
11
  vi.mock('@semiont/core', async () => {
@@ -53,20 +54,28 @@ describe('AssessmentEntry', () => {
53
54
  isFocused: false,
54
55
  };
55
56
 
57
+ // Per-test session/bus — created in beforeEach (a module-scope factory
58
+ // call would hand tests a client that test-utils disposes after the
59
+ // first test). The `session` prop and the `eventBus` the interaction
60
+ // test subscribes come from the SAME factory call.
61
+ let session: SemiontSession;
62
+ let eventBus: EventBus;
63
+
56
64
  beforeEach(() => {
57
65
  vi.clearAllMocks();
66
+ ({ session, eventBus } = createTestSemiontWrapper());
58
67
  mockGetAnnotationExactText.mockReturnValue('Selected passage text');
59
68
  });
60
69
 
61
70
  describe('Rendering', () => {
62
71
  it('should render the selected text in quotes', () => {
63
- renderWithProviders(<AssessmentEntry {...defaultProps} />);
72
+ renderWithProviders(<AssessmentEntry {...defaultProps} session={session} />);
64
73
 
65
74
  expect(screen.getByText(/Selected passage text/)).toBeInTheDocument();
66
75
  });
67
76
 
68
77
  it('should render the assessment body text', () => {
69
- renderWithProviders(<AssessmentEntry {...defaultProps} />);
78
+ renderWithProviders(<AssessmentEntry {...defaultProps} session={session} />);
70
79
 
71
80
  expect(screen.getByText('This passage needs clarification')).toBeInTheDocument();
72
81
  });
@@ -80,7 +89,7 @@ describe('AssessmentEntry', () => {
80
89
  });
81
90
 
82
91
  renderWithProviders(
83
- <AssessmentEntry assessment={assessment} isFocused={false} />
92
+ <AssessmentEntry assessment={assessment} isFocused={false} session={session} />
84
93
  );
85
94
 
86
95
  expect(screen.getByText('Direct body assessment')).toBeInTheDocument();
@@ -95,7 +104,7 @@ describe('AssessmentEntry', () => {
95
104
  });
96
105
 
97
106
  renderWithProviders(
98
- <AssessmentEntry assessment={assessment} isFocused={false} />
107
+ <AssessmentEntry assessment={assessment} isFocused={false} session={session} />
99
108
  );
100
109
 
101
110
  expect(screen.getByText('Array body assessment')).toBeInTheDocument();
@@ -105,14 +114,14 @@ describe('AssessmentEntry', () => {
105
114
  const longText = 'X'.repeat(150);
106
115
  mockGetAnnotationExactText.mockReturnValue(longText);
107
116
 
108
- renderWithProviders(<AssessmentEntry {...defaultProps} />);
117
+ renderWithProviders(<AssessmentEntry {...defaultProps} session={session} />);
109
118
 
110
119
  expect(screen.getByText(new RegExp(`"${'X'.repeat(100)}`))).toBeInTheDocument();
111
120
  expect(screen.getByText(/\.\.\./)).toBeInTheDocument();
112
121
  });
113
122
 
114
123
  it('should show creator name', () => {
115
- renderWithProviders(<AssessmentEntry {...defaultProps} />);
124
+ renderWithProviders(<AssessmentEntry {...defaultProps} session={session} />);
116
125
 
117
126
  expect(screen.getByText(/reviewer@example.com/)).toBeInTheDocument();
118
127
  });
@@ -122,7 +131,7 @@ describe('AssessmentEntry', () => {
122
131
  delete (assessment as Record<string, unknown>).creator;
123
132
 
124
133
  renderWithProviders(
125
- <AssessmentEntry assessment={assessment} isFocused={false} />
134
+ <AssessmentEntry assessment={assessment} isFocused={false} session={session} />
126
135
  );
127
136
 
128
137
  expect(screen.getByText(/Unknown/)).toBeInTheDocument();
@@ -133,7 +142,7 @@ describe('AssessmentEntry', () => {
133
142
  delete (assessment as Record<string, unknown>).body;
134
143
 
135
144
  const { container } = renderWithProviders(
136
- <AssessmentEntry assessment={assessment} isFocused={false} />
145
+ <AssessmentEntry assessment={assessment} isFocused={false} session={session} />
137
146
  );
138
147
 
139
148
  // Body section should not render
@@ -143,7 +152,7 @@ describe('AssessmentEntry', () => {
143
152
  it('should not render quote section when selectedText is empty', () => {
144
153
  mockGetAnnotationExactText.mockReturnValue('');
145
154
 
146
- const { container } = renderWithProviders(<AssessmentEntry {...defaultProps} />);
155
+ const { container } = renderWithProviders(<AssessmentEntry {...defaultProps} session={session} />);
147
156
 
148
157
  expect(container.querySelector('.semiont-annotation-entry__quote')).not.toBeInTheDocument();
149
158
  });
@@ -154,7 +163,7 @@ describe('AssessmentEntry', () => {
154
163
  });
155
164
 
156
165
  renderWithProviders(
157
- <AssessmentEntry assessment={recentAssessment} isFocused={false} />
166
+ <AssessmentEntry assessment={recentAssessment} isFocused={false} session={session} />
158
167
  );
159
168
 
160
169
  expect(screen.getByText(/just now/)).toBeInTheDocument();
@@ -165,12 +174,11 @@ describe('AssessmentEntry', () => {
165
174
  it('should emit browse:click on click', async () => {
166
175
  const clickHandler = vi.fn();
167
176
 
168
- const { container, eventBus } = renderWithProviders(
169
- <AssessmentEntry {...defaultProps} />,
170
- { returnEventBus: true }
177
+ const { container } = renderWithProviders(
178
+ <AssessmentEntry {...defaultProps} session={session} />
171
179
  );
172
180
 
173
- const subscription = eventBus!.get('browse:click').subscribe(clickHandler);
181
+ const subscription = eventBus.get('browse:click').subscribe(clickHandler);
174
182
 
175
183
  const entry = container.firstChild as HTMLElement;
176
184
  await userEvent.click(entry);
@@ -187,7 +195,7 @@ describe('AssessmentEntry', () => {
187
195
  describe('Hover state', () => {
188
196
  it('should apply pulse class when isHovered is true', () => {
189
197
  const { container } = renderWithProviders(
190
- <AssessmentEntry {...defaultProps} isHovered={true} />
198
+ <AssessmentEntry {...defaultProps} session={session} isHovered={true} />
191
199
  );
192
200
 
193
201
  const entry = container.firstChild as HTMLElement;
@@ -196,7 +204,7 @@ describe('AssessmentEntry', () => {
196
204
 
197
205
  it('should not apply pulse class when isHovered is false', () => {
198
206
  const { container } = renderWithProviders(
199
- <AssessmentEntry {...defaultProps} isHovered={false} />
207
+ <AssessmentEntry {...defaultProps} session={session} isHovered={false} />
200
208
  );
201
209
 
202
210
  const entry = container.firstChild as HTMLElement;
@@ -207,7 +215,7 @@ describe('AssessmentEntry', () => {
207
215
  describe('Focus state', () => {
208
216
  it('should set data-focused to true when focused', () => {
209
217
  const { container } = renderWithProviders(
210
- <AssessmentEntry {...defaultProps} isFocused={true} />
218
+ <AssessmentEntry {...defaultProps} session={session} isFocused={true} />
211
219
  );
212
220
 
213
221
  const entry = container.firstChild as HTMLElement;
@@ -215,7 +223,7 @@ describe('AssessmentEntry', () => {
215
223
  });
216
224
 
217
225
  it('should set data-type to assessment', () => {
218
- const { container } = renderWithProviders(<AssessmentEntry {...defaultProps} />);
226
+ const { container } = renderWithProviders(<AssessmentEntry {...defaultProps} session={session} />);
219
227
 
220
228
  const entry = container.firstChild as HTMLElement;
221
229
  expect(entry).toHaveAttribute('data-type', 'assessment');
@@ -6,6 +6,7 @@ import userEvent from '@testing-library/user-event';
6
6
  import '@testing-library/jest-dom';
7
7
  import { AssessmentPanel } from '../AssessmentPanel';
8
8
  import type { EventBus } from '@semiont/core';
9
+ import type { SemiontSession } from '@semiont/sdk';
9
10
  import { createTestSemiontWrapper } from '../../../../test-utils';
10
11
 
11
12
  import type { Annotation, AnnotationId } from '@semiont/core';
@@ -32,8 +33,15 @@ function createEventTracker() {
32
33
  };
33
34
  }
34
35
 
36
+ // Per-test session/bus/wrapper — created in beforeEach (a module-scope
37
+ // factory call would hand tests a client that test-utils disposes after
38
+ // the first test). The `session` passed as a prop below and the `eventBus`
39
+ // the tracker subscribes come from the SAME factory call.
40
+ let session: SemiontSession;
41
+ let eventBus: EventBus;
42
+ let SemiontWrapper: React.ComponentType<{ children: React.ReactNode }>;
43
+
35
44
  const renderWithEventBus = (component: React.ReactElement, tracker?: ReturnType<typeof createEventTracker>) => {
36
- const { SemiontWrapper, eventBus } = createTestSemiontWrapper();
37
45
  if (tracker) tracker._attach(eventBus);
38
46
  const Wrapper = ({ children }: { children: React.ReactNode }) => (
39
47
  <SemiontWrapper>{children}</SemiontWrapper>
@@ -140,6 +148,7 @@ const createPendingAnnotation = (exact: string) => ({
140
148
 
141
149
  describe('AssessmentPanel Component', () => {
142
150
  const defaultProps = {
151
+ resourceId: 'res-1',
143
152
  annotations: mockAssessments.empty,
144
153
  pendingAnnotation: null,
145
154
  };
@@ -147,6 +156,8 @@ describe('AssessmentPanel Component', () => {
147
156
  beforeEach(() => {
148
157
  vi.clearAllMocks();
149
158
 
159
+ ({ session, eventBus, SemiontWrapper } = createTestSemiontWrapper());
160
+
150
161
  // Mock scrollIntoView for jsdom
151
162
  Element.prototype.scrollIntoView = vi.fn();
152
163
 
@@ -166,20 +177,20 @@ describe('AssessmentPanel Component', () => {
166
177
 
167
178
  describe('Rendering', () => {
168
179
  it('should render panel header with title and count', () => {
169
- renderWithEventBus(<AssessmentPanel {...defaultProps} annotations={mockAssessments.multiple} />);
180
+ renderWithEventBus(<AssessmentPanel {...defaultProps} session={session} annotations={mockAssessments.multiple} />);
170
181
 
171
182
  expect(screen.getByText(/Assessments/)).toBeInTheDocument();
172
183
  expect(screen.getByText(/\(3\)/)).toBeInTheDocument();
173
184
  });
174
185
 
175
186
  it('should show empty state when no assessments', () => {
176
- renderWithEventBus(<AssessmentPanel {...defaultProps} />);
187
+ renderWithEventBus(<AssessmentPanel {...defaultProps} session={session} />);
177
188
 
178
189
  expect(screen.getByText(/No assessments yet/)).toBeInTheDocument();
179
190
  });
180
191
 
181
192
  it('should render all assessments', () => {
182
- renderWithEventBus(<AssessmentPanel {...defaultProps} annotations={mockAssessments.multiple} />);
193
+ renderWithEventBus(<AssessmentPanel {...defaultProps} session={session} annotations={mockAssessments.multiple} />);
183
194
 
184
195
  expect(screen.getByTestId('assessment-1')).toBeInTheDocument();
185
196
  expect(screen.getByTestId('assessment-2')).toBeInTheDocument();
@@ -187,7 +198,7 @@ describe('AssessmentPanel Component', () => {
187
198
  });
188
199
 
189
200
  it('should have proper panel structure', () => {
190
- const { container } = renderWithEventBus(<AssessmentPanel {...defaultProps} />);
201
+ const { container } = renderWithEventBus(<AssessmentPanel {...defaultProps} session={session} />);
191
202
 
192
203
  const panel = container.firstChild as HTMLElement;
193
204
  expect(panel).toHaveClass('semiont-panel');
@@ -196,7 +207,7 @@ describe('AssessmentPanel Component', () => {
196
207
 
197
208
  describe('Assessment Sorting', () => {
198
209
  it('should sort assessments by position in resource', () => {
199
- renderWithEventBus(<AssessmentPanel {...defaultProps} annotations={mockAssessments.multiple} />);
210
+ renderWithEventBus(<AssessmentPanel {...defaultProps} session={session} annotations={mockAssessments.multiple} />);
200
211
 
201
212
  const assessments = screen.getAllByTestId(/assessment-/);
202
213
 
@@ -210,14 +221,14 @@ describe('AssessmentPanel Component', () => {
210
221
  mockGetTextPositionSelector.mockReturnValue(null);
211
222
 
212
223
  expect(() => {
213
- renderWithEventBus(<AssessmentPanel {...defaultProps} annotations={mockAssessments.multiple} />);
224
+ renderWithEventBus(<AssessmentPanel {...defaultProps} session={session} annotations={mockAssessments.multiple} />);
214
225
  }).not.toThrow();
215
226
  });
216
227
  });
217
228
 
218
229
  describe('New Assessment Creation', () => {
219
230
  it('should not show new assessment input by default', () => {
220
- renderWithEventBus(<AssessmentPanel {...defaultProps} />);
231
+ renderWithEventBus(<AssessmentPanel {...defaultProps} session={session} />);
221
232
 
222
233
  expect(screen.queryByPlaceholderText(/Type your assessment here/)).not.toBeInTheDocument();
223
234
  });
@@ -227,7 +238,7 @@ describe('AssessmentPanel Component', () => {
227
238
 
228
239
  renderWithEventBus(
229
240
  <AssessmentPanel
230
- {...defaultProps}
241
+ {...defaultProps} session={session}
231
242
  pendingAnnotation={pendingAnnotation}
232
243
  />
233
244
  );
@@ -240,7 +251,7 @@ describe('AssessmentPanel Component', () => {
240
251
 
241
252
  renderWithEventBus(
242
253
  <AssessmentPanel
243
- {...defaultProps}
254
+ {...defaultProps} session={session}
244
255
  pendingAnnotation={pendingAnnotation}
245
256
  />
246
257
  );
@@ -254,7 +265,7 @@ describe('AssessmentPanel Component', () => {
254
265
 
255
266
  renderWithEventBus(
256
267
  <AssessmentPanel
257
- {...defaultProps}
268
+ {...defaultProps} session={session}
258
269
  pendingAnnotation={pendingAnnotation}
259
270
  />
260
271
  );
@@ -268,7 +279,7 @@ describe('AssessmentPanel Component', () => {
268
279
 
269
280
  renderWithEventBus(
270
281
  <AssessmentPanel
271
- {...defaultProps}
282
+ {...defaultProps} session={session}
272
283
  pendingAnnotation={pendingAnnotation}
273
284
  />
274
285
  );
@@ -284,7 +295,7 @@ describe('AssessmentPanel Component', () => {
284
295
 
285
296
  renderWithEventBus(
286
297
  <AssessmentPanel
287
- {...defaultProps}
298
+ {...defaultProps} session={session}
288
299
  pendingAnnotation={pendingAnnotation}
289
300
  />
290
301
  );
@@ -302,7 +313,7 @@ describe('AssessmentPanel Component', () => {
302
313
 
303
314
  renderWithEventBus(
304
315
  <AssessmentPanel
305
- {...defaultProps}
316
+ {...defaultProps} session={session}
306
317
  pendingAnnotation={pendingAnnotation}
307
318
  />
308
319
  );
@@ -316,7 +327,7 @@ describe('AssessmentPanel Component', () => {
316
327
 
317
328
  renderWithEventBus(
318
329
  <AssessmentPanel
319
- {...defaultProps}
330
+ {...defaultProps} session={session}
320
331
  pendingAnnotation={pendingAnnotation}
321
332
  />
322
333
  );
@@ -331,7 +342,7 @@ describe('AssessmentPanel Component', () => {
331
342
 
332
343
  renderWithEventBus(
333
344
  <AssessmentPanel
334
- {...defaultProps}
345
+ {...defaultProps} session={session}
335
346
  pendingAnnotation={pendingAnnotation}
336
347
  />,
337
348
  tracker
@@ -357,7 +368,7 @@ describe('AssessmentPanel Component', () => {
357
368
 
358
369
  renderWithEventBus(
359
370
  <AssessmentPanel
360
- {...defaultProps}
371
+ {...defaultProps} session={session}
361
372
  pendingAnnotation={pendingAnnotation}
362
373
  />
363
374
  );
@@ -375,7 +386,7 @@ describe('AssessmentPanel Component', () => {
375
386
 
376
387
  renderWithEventBus(
377
388
  <AssessmentPanel
378
- {...defaultProps}
389
+ {...defaultProps} session={session}
379
390
  pendingAnnotation={pendingAnnotation}
380
391
  />,
381
392
  tracker
@@ -398,7 +409,7 @@ describe('AssessmentPanel Component', () => {
398
409
 
399
410
  const { container } = renderWithEventBus(
400
411
  <AssessmentPanel
401
- {...defaultProps}
412
+ {...defaultProps} session={session}
402
413
  pendingAnnotation={pendingAnnotation}
403
414
  />
404
415
  );
@@ -413,7 +424,7 @@ describe('AssessmentPanel Component', () => {
413
424
  it('should render assessment entries', () => {
414
425
  renderWithEventBus(
415
426
  <AssessmentPanel
416
- {...defaultProps}
427
+ {...defaultProps} session={session}
417
428
  annotations={mockAssessments.single}
418
429
  />
419
430
  );
@@ -428,7 +439,7 @@ describe('AssessmentPanel Component', () => {
428
439
  expect(() => {
429
440
  renderWithEventBus(
430
441
  <AssessmentPanel
431
- {...defaultProps}
442
+ {...defaultProps} session={session}
432
443
  annotations={mockAssessments.single}
433
444
  />
434
445
  );
@@ -440,7 +451,7 @@ describe('AssessmentPanel Component', () => {
440
451
  it('should render AssistSection when annotateMode is true', () => {
441
452
  renderWithEventBus(
442
453
  <AssessmentPanel
443
- {...defaultProps}
454
+ {...defaultProps} session={session}
444
455
  annotateMode={true}
445
456
  />
446
457
  );
@@ -451,7 +462,7 @@ describe('AssessmentPanel Component', () => {
451
462
  it('should not render AssistSection when annotateMode is false', () => {
452
463
  renderWithEventBus(
453
464
  <AssessmentPanel
454
- {...defaultProps}
465
+ {...defaultProps} session={session}
455
466
  annotateMode={false}
456
467
  />
457
468
  );
@@ -462,7 +473,7 @@ describe('AssessmentPanel Component', () => {
462
473
  it('should render AssistSection with correct annotationType', () => {
463
474
  renderWithEventBus(
464
475
  <AssessmentPanel
465
- {...defaultProps}
476
+ {...defaultProps} session={session}
466
477
  annotateMode={true}
467
478
  />
468
479
  );
@@ -478,7 +489,7 @@ describe('AssessmentPanel Component', () => {
478
489
 
479
490
  renderWithEventBus(
480
491
  <AssessmentPanel
481
- {...defaultProps}
492
+ {...defaultProps} session={session}
482
493
  pendingAnnotation={pendingAnnotation}
483
494
  />
484
495
  );
@@ -491,7 +502,7 @@ describe('AssessmentPanel Component', () => {
491
502
 
492
503
  renderWithEventBus(
493
504
  <AssessmentPanel
494
- {...defaultProps}
505
+ {...defaultProps} session={session}
495
506
  pendingAnnotation={pendingAnnotation}
496
507
  />
497
508
  );
@@ -508,7 +519,7 @@ describe('AssessmentPanel Component', () => {
508
519
 
509
520
  describe('Accessibility', () => {
510
521
  it('should have proper heading structure', () => {
511
- renderWithEventBus(<AssessmentPanel {...defaultProps} />);
522
+ renderWithEventBus(<AssessmentPanel {...defaultProps} session={session} />);
512
523
 
513
524
  const heading = screen.getByText(/Assessments/);
514
525
  expect(heading).toHaveClass('semiont-panel-header__text');
@@ -519,7 +530,7 @@ describe('AssessmentPanel Component', () => {
519
530
 
520
531
  renderWithEventBus(
521
532
  <AssessmentPanel
522
- {...defaultProps}
533
+ {...defaultProps} session={session}
523
534
  pendingAnnotation={pendingAnnotation}
524
535
  />
525
536
  );