@semiont/react-ui 0.5.10 → 0.5.11
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/README.md +1 -1
- package/dist/{PdfAnnotationCanvas.client-P37SXBWQ.js → PdfAnnotationCanvas.client-KWQ7XDWT.js} +4 -1
- package/dist/PdfAnnotationCanvas.client-KWQ7XDWT.js.map +1 -0
- package/dist/{chunk-5LFFAVDI.js → chunk-PFUXQPWJ.js} +96 -96
- package/dist/{chunk-5LFFAVDI.js.map → chunk-PFUXQPWJ.js.map} +1 -1
- package/dist/index.css +150 -112
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +287 -39
- package/dist/index.js +2525 -2284
- package/dist/index.js.map +1 -1
- package/dist/test-utils.js +7 -2
- package/dist/test-utils.js.map +1 -1
- package/package.json +6 -6
- package/src/components/ResourceTagsInline.tsx +94 -11
- package/src/components/StatusDisplay.css +36 -0
- package/src/components/__tests__/ResourceTagsInline.test.tsx +101 -3
- package/src/components/annotation/AnnotateToolbar.tsx +53 -28
- package/src/components/annotation/__tests__/AnnotateToolbar.display.test.tsx +76 -0
- package/src/components/annotation/__tests__/AnnotateToolbar.test.tsx +124 -360
- package/src/components/image-annotation/SvgDrawingCanvas.tsx +5 -1
- package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +5 -0
- package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +6 -6
- package/src/components/resource/AnnotateView.tsx +59 -38
- package/src/components/resource/BrowseView.tsx +159 -133
- package/src/components/resource/ResourceViewer.tsx +119 -128
- package/src/components/resource/__tests__/AnnotateView.embeddable.test.tsx +55 -0
- package/src/components/resource/__tests__/BrowseView.embeddable.test.tsx +49 -0
- package/src/components/resource/__tests__/BrowseView.links.test.tsx +60 -0
- package/src/components/resource/__tests__/BrowseView.overlay-async.test.tsx +113 -0
- package/src/components/resource/__tests__/BrowseView.reference-hover.test.tsx +161 -0
- package/src/components/resource/__tests__/BrowseView.registry.test.tsx +43 -0
- package/src/components/resource/__tests__/BrowseView.test.tsx +35 -22
- package/src/components/resource/__tests__/ResourceViewer.embeddable.test.tsx +63 -0
- package/src/components/resource/__tests__/ResourceViewer.mode-switch.test.tsx +12 -7
- package/src/components/resource/__tests__/ResourceViewer.toolbar-opt-out.test.tsx +135 -0
- package/src/components/resource/__tests__/ResourceViewer.toolbar-prefs.test.tsx +200 -0
- package/src/components/resource/__tests__/browse-renderers.test.tsx +34 -0
- package/src/components/resource/__tests__/inline-embedding.test.tsx +81 -0
- package/src/components/resource/__tests__/media-completeness.test.tsx +88 -0
- package/src/components/resource/browse-renderers.tsx +63 -0
- package/src/components/resource/panels/AssessmentPanel.tsx +4 -0
- package/src/components/resource/panels/CommentsPanel.tsx +4 -0
- package/src/components/resource/panels/HighlightPanel.tsx +4 -0
- package/src/components/resource/panels/ReferencesPanel.tsx +4 -0
- package/src/components/resource/panels/TaggingPanel.tsx +4 -0
- package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +5 -2
- package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +1 -0
- package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +1 -0
- package/src/components/resource/panels/__tests__/HighlightPanel.annotationProgress.test.tsx +16 -16
- package/src/components/resource/panels/__tests__/ReferencesPanel.observable-flow.test.tsx +1 -1
- package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +1 -0
- package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +1 -0
- package/src/components/toolbar/Toolbar.css +14 -0
- package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +7 -17
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +40 -17
- package/src/features/resource-viewer/state/resource-viewer-page-state-unit.ts +2 -14
- package/src/styles/features/resource-viewer.css +19 -0
- package/dist/PdfAnnotationCanvas.client-P37SXBWQ.js.map +0 -1
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EMBEDDABLE-VIEWER-COMPLETION Phase 2b — `onReferenceHover`.
|
|
3
|
+
*
|
|
4
|
+
* Hovering a RESOLVED reference span fires `onReferenceHover({annotation,
|
|
5
|
+
* referent, anchorRect})` once — after the viewer's dwell AND the referent's
|
|
6
|
+
* cached descriptor resolves. A stub fires nothing (not even null); leaving
|
|
7
|
+
* before the descriptor resolves cancels (no fire, and no stray null); leaving
|
|
8
|
+
* after a fire sends `null`; the beckon:hover panel-highlight emit is untouched.
|
|
9
|
+
*
|
|
10
|
+
* Started RED (no `onReferenceHover` prop) and GREEN once Phase 2b lands.
|
|
11
|
+
* Test mechanics mirror BrowseView.test.tsx: mock target with `closest()`,
|
|
12
|
+
* fake timers for the dwell, a BehaviorSubject standing in for the cached
|
|
13
|
+
* `browse.resource` observable so the test controls resolve timing.
|
|
14
|
+
*/
|
|
15
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
16
|
+
import { render, fireEvent } from '@testing-library/react';
|
|
17
|
+
import '@testing-library/jest-dom';
|
|
18
|
+
import { BehaviorSubject } from 'rxjs';
|
|
19
|
+
import type { Annotation, AnnotationId, ResourceDescriptor } from '@semiont/core';
|
|
20
|
+
import type { SemiontSession } from '@semiont/sdk';
|
|
21
|
+
import { BrowseView } from '../BrowseView';
|
|
22
|
+
|
|
23
|
+
vi.mock('../../annotation/AnnotateToolbar', () => ({ AnnotateToolbar: () => null }));
|
|
24
|
+
vi.mock('react-markdown', () => ({ default: ({ children }: { children: string }) => <div>{children}</div> }));
|
|
25
|
+
vi.mock('remark-gfm', () => ({ default: () => ({}) }));
|
|
26
|
+
// jsdom has no Range API — the overlay is not under test here.
|
|
27
|
+
vi.mock('../../../lib/annotation-overlay', () => ({
|
|
28
|
+
buildSourceToRenderedMap: vi.fn(() => new Map()),
|
|
29
|
+
buildTextNodeIndex: vi.fn(() => []),
|
|
30
|
+
resolveAnnotationRanges: vi.fn(() => new Map()),
|
|
31
|
+
applyHighlights: vi.fn(),
|
|
32
|
+
clearHighlights: vi.fn(),
|
|
33
|
+
toOverlayAnnotations: vi.fn(() => []),
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
const REFERENT = { '@id': 'res-target', name: 'Target Doc' } as unknown as ResourceDescriptor;
|
|
37
|
+
const DWELL = 100;
|
|
38
|
+
|
|
39
|
+
const makeAnnotation = (id: string, body: Annotation['body']): Annotation => ({
|
|
40
|
+
'@context': 'http://www.w3.org/ns/anno.jsonld',
|
|
41
|
+
id: id as AnnotationId,
|
|
42
|
+
type: 'Annotation',
|
|
43
|
+
motivation: 'linking',
|
|
44
|
+
creator: { '@type': 'Person', name: 'u' },
|
|
45
|
+
created: '2024-01-01T00:00:00Z',
|
|
46
|
+
target: { source: 'res-1', selector: { type: 'TextPositionSelector', start: 0, end: 4 } },
|
|
47
|
+
body,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const resolvedRef = makeAnnotation('ref-resolved', [{ type: 'SpecificResource', source: 'res-target', purpose: 'linking' }]);
|
|
51
|
+
const stubRef = makeAnnotation('ref-stub', []);
|
|
52
|
+
|
|
53
|
+
function setup(onReferenceHover?: (h: unknown) => void) {
|
|
54
|
+
const referent$ = new BehaviorSubject<ResourceDescriptor | undefined>(undefined);
|
|
55
|
+
const beckonHover = vi.fn();
|
|
56
|
+
const browseResource = vi.fn(() => referent$);
|
|
57
|
+
const session = {
|
|
58
|
+
client: {
|
|
59
|
+
browse: { click: vi.fn(), resource: browseResource },
|
|
60
|
+
beckon: { hover: beckonHover },
|
|
61
|
+
},
|
|
62
|
+
subscribe: () => () => {},
|
|
63
|
+
} as unknown as SemiontSession;
|
|
64
|
+
|
|
65
|
+
const { container } = render(
|
|
66
|
+
<BrowseView
|
|
67
|
+
content="some text"
|
|
68
|
+
mimeType="text/plain"
|
|
69
|
+
resourceUri="res-1"
|
|
70
|
+
annotations={{ highlights: [], references: [resolvedRef, stubRef], assessments: [], comments: [], tags: [] }}
|
|
71
|
+
annotateMode={false}
|
|
72
|
+
hoverDelayMs={DWELL}
|
|
73
|
+
session={session}
|
|
74
|
+
{...(onReferenceHover ? { onReferenceHover } : {})}
|
|
75
|
+
/>,
|
|
76
|
+
);
|
|
77
|
+
const content = container.querySelector('.semiont-browse-view__content')!;
|
|
78
|
+
|
|
79
|
+
const hoverTarget = (id: string) => {
|
|
80
|
+
const el = document.createElement('span');
|
|
81
|
+
el.setAttribute('data-annotation-id', id);
|
|
82
|
+
return { el, target: { closest: vi.fn(() => el) } as unknown as Element };
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
return { content, referent$, beckonHover, browseResource, hoverTarget };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
describe('BrowseView — onReferenceHover (Phase 2b)', () => {
|
|
89
|
+
beforeEach(() => { vi.useFakeTimers(); });
|
|
90
|
+
afterEach(() => { vi.useRealTimers(); });
|
|
91
|
+
|
|
92
|
+
it('fires once after dwell + descriptor resolve, with {annotation, referent, anchorRect}', () => {
|
|
93
|
+
const onReferenceHover = vi.fn();
|
|
94
|
+
const { content, referent$, hoverTarget } = setup(onReferenceHover);
|
|
95
|
+
|
|
96
|
+
fireEvent.mouseOver(content, { target: hoverTarget('ref-resolved').target });
|
|
97
|
+
vi.advanceTimersByTime(DWELL + 10); // dwell fires → subscribe starts
|
|
98
|
+
expect(onReferenceHover).not.toHaveBeenCalled(); // descriptor not resolved yet
|
|
99
|
+
|
|
100
|
+
referent$.next(REFERENT); // descriptor lands
|
|
101
|
+
expect(onReferenceHover).toHaveBeenCalledTimes(1);
|
|
102
|
+
const hover = onReferenceHover.mock.calls[0]![0];
|
|
103
|
+
expect(hover.annotation.id).toBe('ref-resolved');
|
|
104
|
+
expect(hover.referent).toBe(REFERENT);
|
|
105
|
+
expect(typeof hover.anchorRect.top).toBe('number'); // DOMRect from the hovered span
|
|
106
|
+
|
|
107
|
+
referent$.next({ ...REFERENT }); // later cache emissions must NOT re-fire
|
|
108
|
+
expect(onReferenceHover).toHaveBeenCalledTimes(1);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('a stub reference fires nothing — not even null on leave', () => {
|
|
112
|
+
const onReferenceHover = vi.fn();
|
|
113
|
+
const { content, beckonHover, hoverTarget } = setup(onReferenceHover);
|
|
114
|
+
|
|
115
|
+
const { target } = hoverTarget('ref-stub');
|
|
116
|
+
fireEvent.mouseOver(content, { target });
|
|
117
|
+
vi.advanceTimersByTime(DWELL + 10);
|
|
118
|
+
fireEvent.mouseOut(content, { target });
|
|
119
|
+
|
|
120
|
+
expect(onReferenceHover).not.toHaveBeenCalled();
|
|
121
|
+
expect(beckonHover).toHaveBeenCalledWith('ref-stub'); // panel-highlight path unaffected
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('leaving before the descriptor resolves cancels: no fire, and a late resolve stays silent', () => {
|
|
125
|
+
const onReferenceHover = vi.fn();
|
|
126
|
+
const { content, referent$, hoverTarget } = setup(onReferenceHover);
|
|
127
|
+
|
|
128
|
+
const { target } = hoverTarget('ref-resolved');
|
|
129
|
+
fireEvent.mouseOver(content, { target });
|
|
130
|
+
vi.advanceTimersByTime(DWELL + 10); // dwell fired, load in flight
|
|
131
|
+
fireEvent.mouseOut(content, { target }); // leave first
|
|
132
|
+
|
|
133
|
+
referent$.next(REFERENT); // late resolve
|
|
134
|
+
expect(onReferenceHover).not.toHaveBeenCalled(); // cancelled — and no stray null either
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('leaving after a fire sends null', () => {
|
|
138
|
+
const onReferenceHover = vi.fn();
|
|
139
|
+
const { content, referent$, hoverTarget } = setup(onReferenceHover);
|
|
140
|
+
|
|
141
|
+
const { target } = hoverTarget('ref-resolved');
|
|
142
|
+
fireEvent.mouseOver(content, { target });
|
|
143
|
+
vi.advanceTimersByTime(DWELL + 10);
|
|
144
|
+
referent$.next(REFERENT);
|
|
145
|
+
expect(onReferenceHover).toHaveBeenCalledTimes(1);
|
|
146
|
+
|
|
147
|
+
fireEvent.mouseOut(content, { target });
|
|
148
|
+
expect(onReferenceHover).toHaveBeenCalledTimes(2);
|
|
149
|
+
expect(onReferenceHover).toHaveBeenLastCalledWith(null);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('without onReferenceHover, hover still emits beckon:hover and loads nothing', () => {
|
|
153
|
+
const { content, beckonHover, browseResource, hoverTarget } = setup();
|
|
154
|
+
|
|
155
|
+
fireEvent.mouseOver(content, { target: hoverTarget('ref-resolved').target });
|
|
156
|
+
vi.advanceTimersByTime(DWELL + 10);
|
|
157
|
+
|
|
158
|
+
expect(beckonHover).toHaveBeenCalledWith('ref-resolved');
|
|
159
|
+
expect(browseResource).not.toHaveBeenCalled(); // no handler → no referent load
|
|
160
|
+
});
|
|
161
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EMBEDDABLE-RESOURCE-VIEWER step 3 — overridable browse media-renderer registry.
|
|
3
|
+
*
|
|
4
|
+
* A consumer can swap a media renderer via the `renderers` prop (e.g. its own PDF
|
|
5
|
+
* viewer) without forking BrowseView. AnnotateToolbar is mocked (unrelated here).
|
|
6
|
+
*
|
|
7
|
+
* Started RED (tsc: no `renderers` prop) and GREEN once step 3 lands.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
10
|
+
import { render, screen } from '@testing-library/react';
|
|
11
|
+
import '@testing-library/jest-dom';
|
|
12
|
+
import type { SemiontSession } from '@semiont/sdk';
|
|
13
|
+
import { BrowseView } from '../BrowseView';
|
|
14
|
+
import type { MediaRendererProps } from '../browse-renderers';
|
|
15
|
+
|
|
16
|
+
vi.mock('../../annotation/AnnotateToolbar', () => ({ AnnotateToolbar: () => null }));
|
|
17
|
+
|
|
18
|
+
const emptyAnnotations = { highlights: [], references: [], assessments: [], comments: [], tags: [] };
|
|
19
|
+
|
|
20
|
+
function fakeSession(): SemiontSession {
|
|
21
|
+
return {
|
|
22
|
+
client: { browse: { click: vi.fn() }, beckon: { hover: vi.fn() } },
|
|
23
|
+
subscribe: () => () => {},
|
|
24
|
+
} as unknown as SemiontSession;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('BrowseView — media-renderer registry', () => {
|
|
28
|
+
it('uses a custom renderer from the `renderers` override', () => {
|
|
29
|
+
const CustomText = ({ content }: MediaRendererProps) => <div>custom-render: {content}</div>;
|
|
30
|
+
render(
|
|
31
|
+
<BrowseView
|
|
32
|
+
content="the-body"
|
|
33
|
+
mimeType="text/plain"
|
|
34
|
+
resourceUri="res-1"
|
|
35
|
+
annotations={emptyAnnotations}
|
|
36
|
+
annotateMode={false}
|
|
37
|
+
session={fakeSession()}
|
|
38
|
+
renderers={{ text: CustomText }}
|
|
39
|
+
/>,
|
|
40
|
+
);
|
|
41
|
+
expect(screen.getByText(/custom-render: the-body/)).toBeInTheDocument();
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -3,17 +3,15 @@ import React from 'react';
|
|
|
3
3
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
4
4
|
import { BrowseView } from '../BrowseView';
|
|
5
5
|
import type { EventBus } from '@semiont/core';
|
|
6
|
+
import type { SemiontClient, SemiontSession } from '@semiont/sdk';
|
|
6
7
|
import { createTestSemiontWrapper } from '../../../test-utils';
|
|
7
8
|
|
|
8
9
|
import type { Annotation, AnnotationId } from '@semiont/core';
|
|
9
10
|
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
newAnnotationIds: mockNewAnnotationIds,
|
|
15
|
-
})),
|
|
16
|
-
}));
|
|
11
|
+
// BrowseView takes its `session` + `newAnnotationIds` as props now (step 1a) — no
|
|
12
|
+
// ResourceAnnotationsContext / SemiontProvider reach-in. `makeSession` (below)
|
|
13
|
+
// wraps the fake client so browse:click / beckon:hover land on the bus the
|
|
14
|
+
// trackers listen on, and session.subscribe registers beckon:* on that bus.
|
|
17
15
|
|
|
18
16
|
// Mock @semiont/core utilities. The media-type registry (`capabilitiesOf`)
|
|
19
17
|
// is NOT mocked — BrowseView dispatches on the real registry's render mode,
|
|
@@ -116,34 +114,47 @@ function createEventTracker() {
|
|
|
116
114
|
};
|
|
117
115
|
}
|
|
118
116
|
|
|
117
|
+
// BrowseView now takes its session as a prop. Wrap the fake client so
|
|
118
|
+
// browse:click / beckon:hover emit on the SAME bus the trackers listen on, and
|
|
119
|
+
// session.subscribe registers beckon:* there (mirrors test-utils' fake session).
|
|
120
|
+
function makeSession(client: SemiontClient): SemiontSession {
|
|
121
|
+
return {
|
|
122
|
+
client,
|
|
123
|
+
subscribe: (channel: string, handler: (p: never) => void) => {
|
|
124
|
+
const sub = (client.bus.get(channel as never) as { subscribe(fn: (p: never) => void): { unsubscribe(): void } }).subscribe(handler);
|
|
125
|
+
return () => sub.unsubscribe();
|
|
126
|
+
},
|
|
127
|
+
} as unknown as SemiontSession;
|
|
128
|
+
}
|
|
129
|
+
|
|
119
130
|
const renderWithProviders = (
|
|
120
|
-
component: React.ReactElement
|
|
131
|
+
component: React.ReactElement<React.ComponentProps<typeof BrowseView>>,
|
|
121
132
|
options: { newAnnotationIds?: Set<string> } = {}
|
|
122
133
|
) => {
|
|
123
|
-
|
|
124
|
-
mockNewAnnotationIds = options.newAnnotationIds;
|
|
125
|
-
}
|
|
126
|
-
const { SemiontWrapper } = createTestSemiontWrapper();
|
|
134
|
+
const { SemiontWrapper, client } = createTestSemiontWrapper();
|
|
127
135
|
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
128
136
|
<SemiontWrapper>{children}</SemiontWrapper>
|
|
129
137
|
);
|
|
130
|
-
return render(
|
|
138
|
+
return render(
|
|
139
|
+
React.cloneElement(component, { session: makeSession(client), newAnnotationIds: options.newAnnotationIds }),
|
|
140
|
+
{ wrapper: Wrapper },
|
|
141
|
+
);
|
|
131
142
|
};
|
|
132
143
|
|
|
133
144
|
const renderWithEventTracking = (
|
|
134
|
-
component: React.ReactElement
|
|
145
|
+
component: React.ReactElement<React.ComponentProps<typeof BrowseView>>,
|
|
135
146
|
tracker: ReturnType<typeof createEventTracker>,
|
|
136
147
|
options: { newAnnotationIds?: Set<string> } = {}
|
|
137
148
|
) => {
|
|
138
|
-
|
|
139
|
-
mockNewAnnotationIds = options.newAnnotationIds;
|
|
140
|
-
}
|
|
141
|
-
const { SemiontWrapper, eventBus } = createTestSemiontWrapper();
|
|
149
|
+
const { SemiontWrapper, eventBus, client } = createTestSemiontWrapper();
|
|
142
150
|
tracker._attach(eventBus);
|
|
143
151
|
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
144
152
|
<SemiontWrapper>{children}</SemiontWrapper>
|
|
145
153
|
);
|
|
146
|
-
return render(
|
|
154
|
+
return render(
|
|
155
|
+
React.cloneElement(component, { session: makeSession(client), newAnnotationIds: options.newAnnotationIds }),
|
|
156
|
+
{ wrapper: Wrapper },
|
|
157
|
+
);
|
|
147
158
|
};
|
|
148
159
|
|
|
149
160
|
// Test data fixtures
|
|
@@ -179,11 +190,13 @@ describe('BrowseView Component', () => {
|
|
|
179
190
|
hoveredAnnotationId: null,
|
|
180
191
|
selectedClick: 'detail' as const,
|
|
181
192
|
annotateMode: false,
|
|
193
|
+
// Placeholder — the render helpers override this with a session bound to the
|
|
194
|
+
// fake client's bus (so emits/subscriptions land where the trackers listen).
|
|
195
|
+
session: null as SemiontSession | null,
|
|
182
196
|
};
|
|
183
197
|
|
|
184
198
|
beforeEach(() => {
|
|
185
199
|
vi.clearAllMocks();
|
|
186
|
-
mockNewAnnotationIds = new Set();
|
|
187
200
|
|
|
188
201
|
// Mock scrollIntoView for jsdom
|
|
189
202
|
if (typeof Element !== 'undefined') {
|
|
@@ -499,7 +512,7 @@ describe('BrowseView Component', () => {
|
|
|
499
512
|
tags: [],
|
|
500
513
|
};
|
|
501
514
|
|
|
502
|
-
const { SemiontWrapper, eventBus } = createTestSemiontWrapper();
|
|
515
|
+
const { SemiontWrapper, eventBus, client } = createTestSemiontWrapper();
|
|
503
516
|
eventBus.get('beckon:hover').subscribe((payload: any) => {
|
|
504
517
|
eventTracker.push({ event: 'beckon:hover', annotationId: payload?.annotationId ?? null });
|
|
505
518
|
});
|
|
@@ -509,7 +522,7 @@ describe('BrowseView Component', () => {
|
|
|
509
522
|
|
|
510
523
|
const { container } = render(
|
|
511
524
|
<SemiontWrapper>
|
|
512
|
-
<BrowseView {...defaultProps} annotations={manyAnnotations} />
|
|
525
|
+
<BrowseView {...defaultProps} annotations={manyAnnotations} session={makeSession(client)} />
|
|
513
526
|
</SemiontWrapper>
|
|
514
527
|
);
|
|
515
528
|
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EMBEDDABLE-RESOURCE-VIEWER — keystone acceptance spec.
|
|
3
|
+
*
|
|
4
|
+
* The consumer's (my-chat) actual requirement: render + interact with a resource
|
|
5
|
+
* fed ONLY a `SemiontSession`, with NO SemiontProvider / TranslationProvider /
|
|
6
|
+
* cache context mounted. This is the definition of done for provider-free
|
|
7
|
+
* rendering — the "an external host can import the pieces" half of the plan's
|
|
8
|
+
* dual acceptance test.
|
|
9
|
+
*
|
|
10
|
+
* GREEN as of step 1c: the browse-mode subtree (ResourceViewer → BrowseView →
|
|
11
|
+
* AnnotateToolbar) renders provider-free from a bare session. The annotate-mode
|
|
12
|
+
* subtree is covered by AnnotateView.embeddable.test.tsx.
|
|
13
|
+
*/
|
|
14
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
15
|
+
import { render, screen } from '@testing-library/react';
|
|
16
|
+
import '@testing-library/jest-dom';
|
|
17
|
+
import type { SemiontSession } from '@semiont/sdk';
|
|
18
|
+
import type { ResourceDescriptor as SemiontResource, ResourceId } from '@semiont/core';
|
|
19
|
+
import { ResourceViewer } from '../ResourceViewer';
|
|
20
|
+
|
|
21
|
+
// Minimal bring-your-own-session double: just the surface ResourceViewer + its
|
|
22
|
+
// subtree touch — `client.browse` / `client.mark` and generic-channel `subscribe`.
|
|
23
|
+
function fakeSession(): SemiontSession {
|
|
24
|
+
const client = {
|
|
25
|
+
baseUrl: 'http://localhost:4000',
|
|
26
|
+
browse: { invalidateAnnotationList: vi.fn(), click: vi.fn() },
|
|
27
|
+
mark: { delete: vi.fn() },
|
|
28
|
+
};
|
|
29
|
+
return {
|
|
30
|
+
client,
|
|
31
|
+
subscribe: () => () => {},
|
|
32
|
+
} as unknown as SemiontSession;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const resource: SemiontResource & { content: string } = {
|
|
36
|
+
'@context': 'https://www.w3.org/ns/activitystreams',
|
|
37
|
+
'@id': 'res-1' as ResourceId,
|
|
38
|
+
name: 'Doc',
|
|
39
|
+
created: '2024-01-01T00:00:00Z',
|
|
40
|
+
entityTypes: [],
|
|
41
|
+
archived: false,
|
|
42
|
+
representations: [{ mediaType: 'text/plain', byteSize: 10 }],
|
|
43
|
+
content: 'Embeddable content.',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const annotations = { highlights: [], references: [], assessments: [], comments: [], tags: [] };
|
|
47
|
+
|
|
48
|
+
describe('ResourceViewer — embeddable (bring-your-own-session, no providers)', () => {
|
|
49
|
+
// GREEN: the whole browse-mode subtree (ResourceViewer → BrowseView →
|
|
50
|
+
// AnnotateToolbar) renders provider-free from a bare session.
|
|
51
|
+
it('renders content fed only a session, with NO providers mounted', () => {
|
|
52
|
+
render(
|
|
53
|
+
<ResourceViewer
|
|
54
|
+
session={fakeSession()}
|
|
55
|
+
resource={resource}
|
|
56
|
+
annotations={annotations}
|
|
57
|
+
onOpenResource={vi.fn()}
|
|
58
|
+
onOpenPanel={vi.fn()}
|
|
59
|
+
/>,
|
|
60
|
+
);
|
|
61
|
+
expect(screen.getByText('Embeddable content.')).toBeInTheDocument();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -19,14 +19,10 @@ import { TranslationProvider } from '../../../contexts/TranslationContext';
|
|
|
19
19
|
import { ResourceAnnotationsProvider } from '../../../contexts/ResourceAnnotationsContext';
|
|
20
20
|
import type { ResourceDescriptor as SemiontResource, ResourceId } from '@semiont/core';
|
|
21
21
|
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
useObservableExternalNavigation: () => vi.fn(),
|
|
25
|
-
}));
|
|
26
|
-
|
|
27
|
-
// ResourceViewer (and ResourceAnnotationsContext) now resolve the client via
|
|
22
|
+
// ResourceViewer now takes its session as a prop; its children (BrowseView /
|
|
23
|
+
// AnnotateView) and ResourceAnnotationsContext still resolve the client via
|
|
28
24
|
// useSemiont(). Mock useSemiont to emit a minimal session carrying a stub
|
|
29
|
-
// client with the methods
|
|
25
|
+
// client with the methods those children touch. The session also
|
|
30
26
|
// exposes `on` and `emit` stubs so useEventSubscription(s) don't explode.
|
|
31
27
|
const stubClient = {
|
|
32
28
|
browse: { invalidateAnnotationList: vi.fn() },
|
|
@@ -115,6 +111,7 @@ describe('ResourceViewer - Mode Switching', () => {
|
|
|
115
111
|
<ResourceViewer
|
|
116
112
|
resource={mockResource}
|
|
117
113
|
annotations={mockAnnotations}
|
|
114
|
+
session={null}
|
|
118
115
|
/>
|
|
119
116
|
</TestWrapper>
|
|
120
117
|
);
|
|
@@ -128,6 +125,7 @@ describe('ResourceViewer - Mode Switching', () => {
|
|
|
128
125
|
<ResourceViewer
|
|
129
126
|
resource={mockResource}
|
|
130
127
|
annotations={mockAnnotations}
|
|
128
|
+
session={null}
|
|
131
129
|
/>
|
|
132
130
|
</TestWrapper>
|
|
133
131
|
);
|
|
@@ -144,6 +142,7 @@ describe('ResourceViewer - Mode Switching', () => {
|
|
|
144
142
|
<ResourceViewer
|
|
145
143
|
resource={mockResource}
|
|
146
144
|
annotations={mockAnnotations}
|
|
145
|
+
session={null}
|
|
147
146
|
/>
|
|
148
147
|
</TestWrapper>
|
|
149
148
|
);
|
|
@@ -158,6 +157,7 @@ describe('ResourceViewer - Mode Switching', () => {
|
|
|
158
157
|
<ResourceViewer
|
|
159
158
|
resource={mockResource}
|
|
160
159
|
annotations={mockAnnotations}
|
|
160
|
+
session={null}
|
|
161
161
|
/>
|
|
162
162
|
</TestWrapper>
|
|
163
163
|
);
|
|
@@ -169,6 +169,7 @@ describe('ResourceViewer - Mode Switching', () => {
|
|
|
169
169
|
<ResourceViewer
|
|
170
170
|
resource={mockResource}
|
|
171
171
|
annotations={mockAnnotations}
|
|
172
|
+
session={null}
|
|
172
173
|
/>
|
|
173
174
|
</TestWrapper>
|
|
174
175
|
);
|
|
@@ -181,6 +182,7 @@ describe('ResourceViewer - Mode Switching', () => {
|
|
|
181
182
|
<ResourceViewer
|
|
182
183
|
resource={mockResource}
|
|
183
184
|
annotations={mockAnnotations}
|
|
185
|
+
session={null}
|
|
184
186
|
/>
|
|
185
187
|
</TestWrapper>
|
|
186
188
|
);
|
|
@@ -193,6 +195,7 @@ describe('ResourceViewer - Mode Switching', () => {
|
|
|
193
195
|
<ResourceViewer
|
|
194
196
|
resource={mockResource}
|
|
195
197
|
annotations={mockAnnotations}
|
|
198
|
+
session={null}
|
|
196
199
|
/>
|
|
197
200
|
</TestWrapper>
|
|
198
201
|
);
|
|
@@ -209,6 +212,7 @@ describe('ResourceViewer - Mode Switching', () => {
|
|
|
209
212
|
<ResourceViewer
|
|
210
213
|
resource={mockResource}
|
|
211
214
|
annotations={mockAnnotations}
|
|
215
|
+
session={null}
|
|
212
216
|
/>
|
|
213
217
|
</TestWrapper>
|
|
214
218
|
);
|
|
@@ -221,6 +225,7 @@ describe('ResourceViewer - Mode Switching', () => {
|
|
|
221
225
|
<ResourceViewer
|
|
222
226
|
resource={mockResource}
|
|
223
227
|
annotations={mockAnnotations}
|
|
228
|
+
session={null}
|
|
224
229
|
/>
|
|
225
230
|
</TestWrapper>
|
|
226
231
|
);
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VIEWER-TOOLBAR-OPT-OUT — `showToolbar={false}` is a supported opt-out.
|
|
3
|
+
*
|
|
4
|
+
* Tier-2 hosts (controlled props, host-composed controls) hide the built-in
|
|
5
|
+
* bar by CONTRACT, not by CSS-ing react-ui's internals. Hiding the bar must
|
|
6
|
+
* not disable any seam: the keystone here proves annotate-mode selection
|
|
7
|
+
* capture still emits mark.request with the bar gone.
|
|
8
|
+
*
|
|
9
|
+
* Started RED (the prop doesn't exist; bars render everywhere) → GREEN.
|
|
10
|
+
*/
|
|
11
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
12
|
+
import { render, fireEvent, screen, within } from '@testing-library/react';
|
|
13
|
+
import '@testing-library/jest-dom';
|
|
14
|
+
import type { SemiontSession } from '@semiont/sdk';
|
|
15
|
+
import type { ResourceDescriptor as SemiontResource, ResourceId } from '@semiont/core';
|
|
16
|
+
import { ResourceViewer } from '../ResourceViewer';
|
|
17
|
+
|
|
18
|
+
// Render the real content so jsdom text selection can target it.
|
|
19
|
+
vi.mock('../../CodeMirrorRenderer', () => ({
|
|
20
|
+
CodeMirrorRenderer: ({ content }: { content: string }) => <div className="codemirror-renderer">{content}</div>,
|
|
21
|
+
}));
|
|
22
|
+
vi.mock('../../image-annotation/SvgDrawingCanvas', () => ({ SvgDrawingCanvas: () => <div>svg-mock</div> }));
|
|
23
|
+
vi.mock('../../pdf-annotation/PdfAnnotationCanvas.client', () => ({ PdfAnnotationCanvas: () => <div>pdf-mock</div> }));
|
|
24
|
+
vi.mock('react-markdown', () => ({ default: ({ children }: { children: string }) => <div>{children}</div> }));
|
|
25
|
+
vi.mock('remark-gfm', () => ({ default: () => ({}) }));
|
|
26
|
+
|
|
27
|
+
const CONTENT = 'hello world selectme end';
|
|
28
|
+
|
|
29
|
+
function makeResource(mediaType = 'text/plain'): SemiontResource & { content: string } {
|
|
30
|
+
return {
|
|
31
|
+
'@context': 'https://www.w3.org/ns/activitystreams',
|
|
32
|
+
'@id': 'res-1' as ResourceId,
|
|
33
|
+
name: 'Doc',
|
|
34
|
+
created: '2024-01-01T00:00:00Z',
|
|
35
|
+
entityTypes: [],
|
|
36
|
+
archived: false,
|
|
37
|
+
representations: [{ mediaType, byteSize: 10 }],
|
|
38
|
+
content: CONTENT,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const annotations = { highlights: [], references: [], assessments: [], comments: [], tags: [] };
|
|
43
|
+
|
|
44
|
+
function fakeSession() {
|
|
45
|
+
const client = {
|
|
46
|
+
browse: { click: vi.fn(), invalidateAnnotationList: vi.fn() },
|
|
47
|
+
beckon: { hover: vi.fn() },
|
|
48
|
+
mark: { request: vi.fn(), delete: vi.fn() },
|
|
49
|
+
};
|
|
50
|
+
return { session: { client, subscribe: () => () => {} } as unknown as SemiontSession, client };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const bars = (c: HTMLElement) => c.querySelectorAll('.semiont-annotate-toolbar').length;
|
|
54
|
+
|
|
55
|
+
describe('ResourceViewer — showToolbar opt-out', () => {
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
vi.clearAllMocks();
|
|
58
|
+
window.getSelection()?.removeAllRanges();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('default pins today’s behavior: the bar renders in browse and annotate modes', () => {
|
|
62
|
+
const { session } = fakeSession();
|
|
63
|
+
const browse = render(
|
|
64
|
+
<ResourceViewer session={session} resource={makeResource()} annotations={annotations} />,
|
|
65
|
+
);
|
|
66
|
+
expect(bars(browse.container)).toBe(1);
|
|
67
|
+
|
|
68
|
+
const annotate = render(
|
|
69
|
+
<ResourceViewer session={session} resource={makeResource()} annotations={annotations}
|
|
70
|
+
annotateMode={true} onAnnotateModeChange={vi.fn()} />,
|
|
71
|
+
);
|
|
72
|
+
expect(bars(annotate.container)).toBe(1);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('showToolbar={false}: no bar in browse mode', () => {
|
|
76
|
+
const { session } = fakeSession();
|
|
77
|
+
const { container } = render(
|
|
78
|
+
<ResourceViewer session={session} resource={makeResource()} annotations={annotations}
|
|
79
|
+
showToolbar={false} />,
|
|
80
|
+
);
|
|
81
|
+
expect(bars(container)).toBe(0);
|
|
82
|
+
expect(screen.getByText(CONTENT)).toBeInTheDocument(); // content still renders
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('showToolbar={false}: no bar in any annotate render mode (text / image / pdf)', async () => {
|
|
86
|
+
const { session } = fakeSession();
|
|
87
|
+
const text = render(
|
|
88
|
+
<ResourceViewer session={session} resource={makeResource('text/plain')} annotations={annotations}
|
|
89
|
+
annotateMode={true} onAnnotateModeChange={vi.fn()} showToolbar={false} />,
|
|
90
|
+
);
|
|
91
|
+
expect(bars(text.container)).toBe(0);
|
|
92
|
+
|
|
93
|
+
const image = render(
|
|
94
|
+
<ResourceViewer session={session} resource={makeResource('image/png')} annotations={annotations}
|
|
95
|
+
annotateMode={true} onAnnotateModeChange={vi.fn()} showToolbar={false} />,
|
|
96
|
+
);
|
|
97
|
+
expect(within(image.container).getByText('svg-mock')).toBeInTheDocument();
|
|
98
|
+
expect(bars(image.container)).toBe(0);
|
|
99
|
+
|
|
100
|
+
const pdf = render(
|
|
101
|
+
<ResourceViewer session={session} resource={makeResource('application/pdf')} annotations={annotations}
|
|
102
|
+
annotateMode={true} onAnnotateModeChange={vi.fn()} showToolbar={false} />,
|
|
103
|
+
);
|
|
104
|
+
await within(pdf.container).findByText('pdf-mock'); // flush the lazy canvas
|
|
105
|
+
expect(bars(pdf.container)).toBe(0);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('keystone: with the bar hidden, annotate-mode selection still emits mark.request', () => {
|
|
109
|
+
const { session, client } = fakeSession();
|
|
110
|
+
const { container } = render(
|
|
111
|
+
<ResourceViewer session={session} resource={makeResource('text/plain')} annotations={annotations}
|
|
112
|
+
annotateMode={true} onAnnotateModeChange={vi.fn()}
|
|
113
|
+
selectionMotivation="highlighting" onSelectionMotivationChange={vi.fn()}
|
|
114
|
+
showToolbar={false} />,
|
|
115
|
+
);
|
|
116
|
+
expect(bars(container)).toBe(0);
|
|
117
|
+
|
|
118
|
+
// Select "world" (offsets 6..11) inside the rendered content, then mouseup.
|
|
119
|
+
const contentEl = within(container).getByText(CONTENT);
|
|
120
|
+
const range = document.createRange();
|
|
121
|
+
range.setStart(contentEl.firstChild!, 6);
|
|
122
|
+
range.setEnd(contentEl.firstChild!, 11);
|
|
123
|
+
const selection = window.getSelection()!;
|
|
124
|
+
selection.removeAllRanges();
|
|
125
|
+
selection.addRange(range);
|
|
126
|
+
|
|
127
|
+
fireEvent.mouseUp(contentEl);
|
|
128
|
+
|
|
129
|
+
expect(client.mark.request).toHaveBeenCalledTimes(1);
|
|
130
|
+
const [source, selectors, motivation] = client.mark.request.mock.calls[0]!;
|
|
131
|
+
expect(String(source)).toBe('res-1');
|
|
132
|
+
expect(selectors).toBeTruthy();
|
|
133
|
+
expect(motivation).toBe('highlighting');
|
|
134
|
+
});
|
|
135
|
+
});
|