@semiont/react-ui 0.5.22 → 0.5.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{ar-ZHKO4XDR.js → ar-JRTKCMTF.js} +11 -4
- package/dist/ar-JRTKCMTF.js.map +1 -0
- package/dist/{bn-3BUDKAK5.js → bn-LO6EDSCC.js} +11 -4
- package/dist/bn-LO6EDSCC.js.map +1 -0
- package/dist/{chunk-PFUXQPWJ.js → chunk-4QMTEPQ3.js} +92 -60
- package/dist/chunk-4QMTEPQ3.js.map +1 -0
- package/dist/{chunk-R52YAGHK.js → chunk-5Y5JDCKC.js} +11 -4
- package/dist/chunk-5Y5JDCKC.js.map +1 -0
- package/dist/{cs-2EFEUKTW.js → cs-LCHHOR4O.js} +11 -4
- package/dist/cs-LCHHOR4O.js.map +1 -0
- package/dist/{da-SUD3Q44Q.js → da-TARGPMJR.js} +11 -4
- package/dist/da-TARGPMJR.js.map +1 -0
- package/dist/{de-6IC5QRX7.js → de-SBH4QBPJ.js} +11 -4
- package/dist/de-SBH4QBPJ.js.map +1 -0
- package/dist/{el-Q4NAXFRC.js → el-2D2OLGWB.js} +11 -4
- package/dist/el-2D2OLGWB.js.map +1 -0
- package/dist/{en-P2L4X27N.js → en-QFIEG3CA.js} +2 -2
- package/dist/{es-YUKZBD4W.js → es-U6YK2LX6.js} +11 -4
- package/dist/es-U6YK2LX6.js.map +1 -0
- package/dist/{fa-OJ47LILE.js → fa-S7SZE37M.js} +11 -4
- package/dist/fa-S7SZE37M.js.map +1 -0
- package/dist/{fi-QFFL5CUB.js → fi-CBL3NIXN.js} +11 -4
- package/dist/fi-CBL3NIXN.js.map +1 -0
- package/dist/{fr-BOQZZWP4.js → fr-EABUHV6S.js} +11 -4
- package/dist/fr-EABUHV6S.js.map +1 -0
- package/dist/{he-LXDY3TFA.js → he-THWD244Y.js} +11 -4
- package/dist/he-THWD244Y.js.map +1 -0
- package/dist/{hi-HOJER77V.js → hi-GHGFZJ44.js} +11 -4
- package/dist/hi-GHGFZJ44.js.map +1 -0
- package/dist/{id-I5WQ6EWD.js → id-POZOUMDW.js} +11 -4
- package/dist/id-POZOUMDW.js.map +1 -0
- package/dist/index.d.ts +269 -69
- package/dist/index.js +2080 -1861
- package/dist/index.js.map +1 -1
- package/dist/{it-UYH642IJ.js → it-KDYJADSH.js} +11 -4
- package/dist/it-KDYJADSH.js.map +1 -0
- package/dist/{ja-JTVWAC4L.js → ja-Z5Z6ZF3P.js} +11 -4
- package/dist/ja-Z5Z6ZF3P.js.map +1 -0
- package/dist/{ko-IHXFMOIZ.js → ko-7X3XUXRN.js} +11 -4
- package/dist/ko-7X3XUXRN.js.map +1 -0
- package/dist/{ms-JH25VFLO.js → ms-BTWEIW43.js} +11 -4
- package/dist/ms-BTWEIW43.js.map +1 -0
- package/dist/{nl-SURXW7IQ.js → nl-IS2CGF5I.js} +11 -4
- package/dist/nl-IS2CGF5I.js.map +1 -0
- package/dist/{no-HXIKQFDZ.js → no-AAMEVA26.js} +11 -4
- package/dist/no-AAMEVA26.js.map +1 -0
- package/dist/{pl-7A4KU6XK.js → pl-PVXUQYFT.js} +11 -4
- package/dist/pl-PVXUQYFT.js.map +1 -0
- package/dist/{pt-6MGYP7Q5.js → pt-3ITQ76TL.js} +11 -4
- package/dist/pt-3ITQ76TL.js.map +1 -0
- package/dist/{ro-IROCHNIA.js → ro-FRB4IIVS.js} +11 -4
- package/dist/ro-FRB4IIVS.js.map +1 -0
- package/dist/{sv-WDJD75CV.js → sv-DS3RO24M.js} +11 -4
- package/dist/sv-DS3RO24M.js.map +1 -0
- package/dist/test-utils.js +9 -3
- package/dist/test-utils.js.map +1 -1
- package/dist/{th-RIO2E6B3.js → th-AX66MWWB.js} +11 -4
- package/dist/th-AX66MWWB.js.map +1 -0
- package/dist/{tr-TZ7ERCIN.js → tr-7KTB67TT.js} +11 -4
- package/dist/tr-7KTB67TT.js.map +1 -0
- package/dist/{uk-ZFWDKLUE.js → uk-SMQS2MAQ.js} +11 -4
- package/dist/uk-SMQS2MAQ.js.map +1 -0
- package/dist/{vi-UIOSP2JG.js → vi-D3JXL2EZ.js} +11 -4
- package/dist/vi-D3JXL2EZ.js.map +1 -0
- package/dist/{zh-6R6YUY46.js → zh-S754AWFS.js} +11 -4
- package/dist/zh-S754AWFS.js.map +1 -0
- package/package.json +4 -4
- package/src/components/annotation-popups/JsonLdView.tsx +1 -1
- package/src/components/annotation-popups/__tests__/JsonLdView.test.tsx +8 -2
- package/src/components/modals/ReferenceWizardModal.tsx +2 -1
- package/src/components/modals/ResourceGenerateModal.tsx +10 -4
- package/src/components/modals/ResourceSearchModal.tsx +3 -1
- package/src/components/modals/SearchModal.tsx +3 -1
- package/src/components/modals/__tests__/ResourceGenerateModal.test.tsx +5 -0
- package/src/components/modals/__tests__/ResourceSearchModal.test.tsx +2 -1
- package/src/components/modals/__tests__/SearchModal.search-wiring.test.tsx +2 -1
- package/src/components/resource/AnnotateView.tsx +70 -138
- package/src/components/resource/AnnotationHistory.tsx +53 -17
- package/src/components/resource/BrowseView.tsx +3 -2
- package/src/components/resource/ResourceViewer.tsx +39 -22
- package/src/components/resource/__tests__/AnnotateView.registry.test.tsx +114 -0
- package/src/components/resource/__tests__/AnnotationHistory.test.tsx +63 -41
- package/src/components/resource/__tests__/BrowseView.reference-hover.test.tsx +2 -1
- package/src/components/resource/__tests__/ResourceViewer.multi-mount-invalidation.test.tsx +140 -0
- package/src/components/resource/__tests__/ResourceViewer.registry.test.tsx +89 -0
- package/src/components/resource/annotate-renderers.tsx +155 -0
- package/src/components/resource/panels/JsonLdPanel.tsx +1 -1
- package/src/components/resource/panels/ReferencesPanel.tsx +38 -3
- package/src/components/resource/panels/TagEntry.tsx +5 -1
- package/src/components/resource/panels/TaggingPanel.tsx +6 -1
- package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +41 -0
- package/src/components/resource/panels/__tests__/JsonLdPanel.test.tsx +8 -2
- package/src/components/resource/panels/__tests__/ReferencesPanel.headless.test.tsx +90 -0
- package/src/components/resource/panels/__tests__/TagEntry.test.tsx +2 -2
- package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +2 -2
- package/src/components/resource/panels/__tests__/UnifiedAnnotationsPanel.failure.test.tsx +78 -0
- package/src/components/settings/SettingsPanel.tsx +6 -2
- package/src/components/settings/__tests__/SettingsPanel.test.tsx +28 -3
- package/src/features/admin-devops/__tests__/AdminDevOpsPage.test.tsx +3 -11
- package/src/features/admin-devops/components/AdminDevOpsPage.tsx +0 -3
- package/src/features/admin-exchange/__tests__/AdminExchangePage.test.tsx +0 -1
- package/src/features/admin-exchange/components/AdminExchangePage.tsx +0 -3
- package/src/features/admin-exchange/state/__tests__/exchange-state-unit.test.ts +1 -1
- package/src/features/admin-security/__tests__/AdminSecurityPage.test.tsx +0 -8
- package/src/features/admin-security/components/AdminSecurityPage.tsx +0 -3
- package/src/features/admin-security/state/__tests__/admin-security-state-unit.test.ts +10 -9
- package/src/features/admin-security/state/admin-security-state-unit.ts +3 -2
- package/src/features/admin-users/__tests__/AdminUsersPage.test.tsx +0 -1
- package/src/features/admin-users/components/AdminUsersPage.tsx +0 -3
- package/src/features/admin-users/state/__tests__/admin-users-state-unit.test.ts +7 -6
- package/src/features/admin-users/state/admin-users-state-unit.ts +3 -2
- package/src/features/auth-welcome/state/__tests__/welcome-state-unit.test.ts +36 -26
- package/src/features/auth-welcome/state/welcome-state-unit.ts +7 -2
- package/src/features/moderate-entity-tags/__tests__/EntityTagsPage.test.tsx +0 -2
- package/src/features/moderate-entity-tags/components/EntityTagsPage.tsx +0 -3
- package/src/features/moderate-entity-tags/state/__tests__/entity-tags-state-unit.test.ts +37 -12
- package/src/features/moderate-entity-tags/state/entity-tags-state-unit.ts +9 -10
- package/src/features/moderate-recent/__tests__/RecentDocumentsPage.test.tsx +3 -5
- package/src/features/moderate-recent/components/RecentDocumentsPage.tsx +0 -3
- package/src/features/moderate-tag-schemas/__tests__/TagSchemasPage.test.tsx +3 -5
- package/src/features/moderate-tag-schemas/components/TagSchemasPage.tsx +0 -3
- package/src/features/moderation-linked-data/__tests__/LinkedDataPage.test.tsx +0 -1
- package/src/features/moderation-linked-data/components/LinkedDataPage.tsx +0 -3
- package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +17 -7
- package/src/features/resource-compose/components/ResourceComposePage.tsx +5 -3
- package/src/features/resource-compose/state/__tests__/compose-page-state-unit.test.ts +33 -12
- package/src/features/resource-compose/state/compose-page-state-unit.ts +9 -8
- package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +3 -2
- package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +25 -3
- package/src/features/resource-discovery/state/__tests__/discover-state-unit.test.ts +90 -25
- package/src/features/resource-discovery/state/discover-state-unit.ts +30 -30
- package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +9 -5
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +70 -40
- package/src/features/resource-viewer/state/__tests__/resource-loader-state-unit.test.ts +111 -8
- package/src/features/resource-viewer/state/__tests__/resource-viewer-page-state-unit.test.ts +167 -27
- package/src/features/resource-viewer/state/resource-loader-state-unit.ts +74 -9
- package/src/features/resource-viewer/state/resource-viewer-page-state-unit.ts +29 -28
- package/translations/ar.json +10 -3
- package/translations/bn.json +10 -3
- package/translations/cs.json +10 -3
- package/translations/da.json +10 -3
- package/translations/de.json +10 -3
- package/translations/el.json +10 -3
- package/translations/en.json +10 -3
- package/translations/es.json +10 -3
- package/translations/fa.json +10 -3
- package/translations/fi.json +10 -3
- package/translations/fr.json +10 -3
- package/translations/he.json +10 -3
- package/translations/hi.json +10 -3
- package/translations/id.json +10 -3
- package/translations/it.json +10 -3
- package/translations/ja.json +10 -3
- package/translations/ko.json +10 -3
- package/translations/ms.json +10 -3
- package/translations/nl.json +10 -3
- package/translations/no.json +10 -3
- package/translations/pl.json +10 -3
- package/translations/pt.json +10 -3
- package/translations/ro.json +10 -3
- package/translations/sv.json +10 -3
- package/translations/th.json +10 -3
- package/translations/tr.json +10 -3
- package/translations/uk.json +10 -3
- package/translations/vi.json +10 -3
- package/translations/zh.json +10 -3
- package/dist/ar-ZHKO4XDR.js.map +0 -1
- package/dist/bn-3BUDKAK5.js.map +0 -1
- package/dist/chunk-PFUXQPWJ.js.map +0 -1
- package/dist/chunk-R52YAGHK.js.map +0 -1
- package/dist/cs-2EFEUKTW.js.map +0 -1
- package/dist/da-SUD3Q44Q.js.map +0 -1
- package/dist/de-6IC5QRX7.js.map +0 -1
- package/dist/el-Q4NAXFRC.js.map +0 -1
- package/dist/es-YUKZBD4W.js.map +0 -1
- package/dist/fa-OJ47LILE.js.map +0 -1
- package/dist/fi-QFFL5CUB.js.map +0 -1
- package/dist/fr-BOQZZWP4.js.map +0 -1
- package/dist/he-LXDY3TFA.js.map +0 -1
- package/dist/hi-HOJER77V.js.map +0 -1
- package/dist/id-I5WQ6EWD.js.map +0 -1
- package/dist/it-UYH642IJ.js.map +0 -1
- package/dist/ja-JTVWAC4L.js.map +0 -1
- package/dist/ko-IHXFMOIZ.js.map +0 -1
- package/dist/ms-JH25VFLO.js.map +0 -1
- package/dist/nl-SURXW7IQ.js.map +0 -1
- package/dist/no-HXIKQFDZ.js.map +0 -1
- package/dist/pl-7A4KU6XK.js.map +0 -1
- package/dist/pt-6MGYP7Q5.js.map +0 -1
- package/dist/ro-IROCHNIA.js.map +0 -1
- package/dist/sv-WDJD75CV.js.map +0 -1
- package/dist/th-RIO2E6B3.js.map +0 -1
- package/dist/tr-TZ7ERCIN.js.map +0 -1
- package/dist/uk-ZFWDKLUE.js.map +0 -1
- package/dist/vi-UIOSP2JG.js.map +0 -1
- package/dist/zh-6R6YUY46.js.map +0 -1
- /package/dist/{en-P2L4X27N.js.map → en-QFIEG3CA.js.map} +0 -0
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
import React, { useState, useCallback, useRef, useMemo } from 'react';
|
|
4
4
|
import { useTranslations } from '../../contexts/TranslationContext';
|
|
5
5
|
import { AnnotateView, type SelectionMotivation, type ClickAction, type ShapeType } from './AnnotateView';
|
|
6
|
+
import type { AnnotateMediaRenderers } from './annotate-renderers';
|
|
6
7
|
import { BrowseView, type ReferenceHover } from './BrowseView';
|
|
8
|
+
import type { BrowseMediaRenderers } from './browse-renderers';
|
|
7
9
|
import { PopupContainer } from '../annotation-popups/SharedPopupElements';
|
|
8
10
|
import { JsonLdView } from '../annotation-popups/JsonLdView';
|
|
9
11
|
import type { Annotation, AnnotationId, ResourceDescriptor as SemiontResource, components, EventMap, AnchorRect } from '@semiont/core';
|
|
@@ -16,19 +18,21 @@ import type { AnnotationsCollection } from '../../types/annotation-props';
|
|
|
16
18
|
/**
|
|
17
19
|
* ResourceViewer - Display and interact with resource content and annotations
|
|
18
20
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* -
|
|
21
|
+
* Real-time updates arrive through the sdk, not through this component:
|
|
22
|
+
* the client's BrowseNamespace invalidates the right resource's annotation
|
|
23
|
+
* list on mark:added/mark:removed (payload-keyed) and patches
|
|
24
|
+
* mark:body-updated in place, so the live queries feeding `annotations`
|
|
25
|
+
* refresh on their own. The viewer subscribes only to `browse:click`
|
|
26
|
+
* (annotation-click routing).
|
|
23
27
|
*
|
|
24
28
|
* Bring-your-own-session: the `session` (SemiontSession) and the host's
|
|
25
29
|
* navigation / panel callbacks come in as props — no SemiontProvider required.
|
|
26
|
-
* Every event it subscribes to is session-scoped
|
|
27
|
-
*
|
|
28
|
-
*
|
|
30
|
+
* Every event it subscribes to is session-scoped and reaches it via
|
|
31
|
+
* `session.subscribe`. Translations fall back to built-in English when no
|
|
32
|
+
* TranslationProvider is mounted; caching flows through `session.client.browse.*`.
|
|
29
33
|
*
|
|
30
34
|
* Event flow:
|
|
31
|
-
* make-meaning → EventLog → SSE → EventBus →
|
|
35
|
+
* make-meaning → EventLog → SSE → EventBus → sdk cache invalidation → live queries
|
|
32
36
|
*/
|
|
33
37
|
interface Props {
|
|
34
38
|
resource: SemiontResource & { content: string };
|
|
@@ -75,6 +79,22 @@ interface Props {
|
|
|
75
79
|
* this makes that opt-out a contract instead of a CSS override.
|
|
76
80
|
*/
|
|
77
81
|
showToolbar?: boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Override the read-only / annotating media renderers respectively; each is
|
|
84
|
+
* merged over its defaults by the view below.
|
|
85
|
+
*
|
|
86
|
+
* Two flat props rather than one nested object: the registries are
|
|
87
|
+
* separately typed (`MediaRendererProps` vs `AnnotateMediaRendererProps`)
|
|
88
|
+
* and nesting them would imply an interchangeability that does not exist.
|
|
89
|
+
*
|
|
90
|
+
* Forwarding them is what makes the registries reachable at all for a host
|
|
91
|
+
* that imports `ResourceViewer` — the documented entry point. Before this,
|
|
92
|
+
* reaching `BrowseView`'s `renderers` meant dropping to `BrowseView`
|
|
93
|
+
* directly and reimplementing the browse/annotate switching this component
|
|
94
|
+
* exists to provide. See .plans/ANNOTATE-RENDERER-REGISTRY.md (D5)
|
|
95
|
+
*/
|
|
96
|
+
browseRenderers?: BrowseMediaRenderers;
|
|
97
|
+
annotateRenderers?: AnnotateMediaRenderers;
|
|
78
98
|
}
|
|
79
99
|
|
|
80
100
|
/**
|
|
@@ -82,9 +102,6 @@ interface Props {
|
|
|
82
102
|
* panel opening invokes the host's `onOpenPanel` callback — the host owns
|
|
83
103
|
* the panel and any `panel:open` emission.
|
|
84
104
|
*
|
|
85
|
-
* @subscribes mark:added - New annotation was added. Payload: { annotation: Annotation }
|
|
86
|
-
* @subscribes mark:removed - Annotation was removed. Payload: { annotationId: string }
|
|
87
|
-
* @subscribes mark:body-updated - Annotation was updated. Payload: { annotation: Annotation }
|
|
88
105
|
* @subscribes browse:click - User clicked on annotation. Payload: { annotationId: string, motivation, anchorRect? }
|
|
89
106
|
*/
|
|
90
107
|
export function ResourceViewer({
|
|
@@ -110,6 +127,8 @@ export function ResourceViewer({
|
|
|
110
127
|
shape: shapeProp,
|
|
111
128
|
onShapeChange,
|
|
112
129
|
showToolbar = true,
|
|
130
|
+
browseRenderers,
|
|
131
|
+
annotateRenderers,
|
|
113
132
|
}: Props) {
|
|
114
133
|
const t = useTranslations('ResourceViewer');
|
|
115
134
|
const documentViewerRef = useRef<HTMLDivElement>(null);
|
|
@@ -140,12 +159,13 @@ export function ResourceViewer({
|
|
|
140
159
|
// Determine active view based on annotate mode
|
|
141
160
|
const activeView = annotateMode ? 'annotate' : 'browse';
|
|
142
161
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
162
|
+
// NOTE deliberately NO mark:* invalidation here (MULTI-RESOURCE-SCOPE
|
|
163
|
+
// Step 5). The sdk's BrowseNamespace already invalidates the RIGHT
|
|
164
|
+
// resource's annotation list on mark:added/mark:removed (payload-keyed,
|
|
165
|
+
// once per event) and patches mark:body-updated in place. A viewer-side
|
|
166
|
+
// handler on these session-wide channels was pure duplication — and with
|
|
167
|
+
// N viewers on one session (the embeddable resource-per-chat-message
|
|
168
|
+
// pattern), an O(N) refetch amplification on every mark event.
|
|
149
169
|
|
|
150
170
|
// Remaining toolbar preferences — same controlled/uncontrolled split as mode.
|
|
151
171
|
const [internalSelectionMotivation, setInternalSelectionMotivation] = useState<SelectionMotivation | null>('linking');
|
|
@@ -286,11 +306,6 @@ export function ResourceViewer({
|
|
|
286
306
|
|
|
287
307
|
// Single subscription call per file (see scripts/compliance/audit-hooks-ordering.ts).
|
|
288
308
|
useSessionEventSubscriptions(session, {
|
|
289
|
-
// Annotation cache invalidation
|
|
290
|
-
'mark:added': handleAnnotationsChanged,
|
|
291
|
-
'mark:removed': handleAnnotationsChanged,
|
|
292
|
-
'mark:body-updated': handleAnnotationsChanged,
|
|
293
|
-
|
|
294
309
|
// Annotation clicks
|
|
295
310
|
'browse:click': handleAnnotationClickEvent,
|
|
296
311
|
});
|
|
@@ -343,6 +358,7 @@ export function ResourceViewer({
|
|
|
343
358
|
showToolbar={showToolbar}
|
|
344
359
|
session={session}
|
|
345
360
|
newAnnotationIds={newAnnotationIds}
|
|
361
|
+
{...(annotateRenderers && { renderers: annotateRenderers })}
|
|
346
362
|
/>
|
|
347
363
|
) : (
|
|
348
364
|
<BrowseView
|
|
@@ -361,6 +377,7 @@ export function ResourceViewer({
|
|
|
361
377
|
onLinkClick={onLinkClick}
|
|
362
378
|
onReferenceHover={onReferenceHover}
|
|
363
379
|
inline={inline}
|
|
380
|
+
{...(browseRenderers && { renderers: browseRenderers })}
|
|
364
381
|
/>
|
|
365
382
|
)}
|
|
366
383
|
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ANNOTATE-RENDERER-REGISTRY — the annotate path's overridable media registry,
|
|
3
|
+
* symmetric to EMBEDDABLE-RESOURCE-VIEWER step 3's browse registry.
|
|
4
|
+
*
|
|
5
|
+
* A host that brings its own PDF stack can already replace the read-only
|
|
6
|
+
* renderer via `BrowseView`'s `renderers` prop; without the same seam here it
|
|
7
|
+
* ends up running two PDF engines depending on which mode the user is in.
|
|
8
|
+
*
|
|
9
|
+
* Started RED (tsc: no `renderers` prop on AnnotateView).
|
|
10
|
+
* See .plans/ANNOTATE-RENDERER-REGISTRY.md
|
|
11
|
+
*/
|
|
12
|
+
import React from 'react';
|
|
13
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
14
|
+
import { render, screen } from '@testing-library/react';
|
|
15
|
+
import '@testing-library/jest-dom';
|
|
16
|
+
import type { SemiontSession } from '@semiont/sdk';
|
|
17
|
+
import type { AnnotationUIState } from '../../../types/annotation-props';
|
|
18
|
+
import { AnnotateView } from '../AnnotateView';
|
|
19
|
+
import type { AnnotateMediaRendererProps } from '../annotate-renderers';
|
|
20
|
+
|
|
21
|
+
vi.mock('../../annotation/AnnotateToolbar', () => ({ AnnotateToolbar: () => null }));
|
|
22
|
+
vi.mock('../../CodeMirrorRenderer', () => ({
|
|
23
|
+
CodeMirrorRenderer: () => <div data-testid="default-text">default-cm</div>,
|
|
24
|
+
}));
|
|
25
|
+
vi.mock('../../image-annotation/SvgDrawingCanvas', () => ({
|
|
26
|
+
SvgDrawingCanvas: () => <div data-testid="default-image">default-svg</div>,
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
const emptyAnnotations = { highlights: [], references: [], assessments: [], comments: [], tags: [] };
|
|
30
|
+
const uiState: AnnotationUIState = {
|
|
31
|
+
selectedMotivation: 'linking',
|
|
32
|
+
selectedClick: 'detail',
|
|
33
|
+
selectedShape: 'rectangle',
|
|
34
|
+
hoveredAnnotationId: null,
|
|
35
|
+
scrollToAnnotationId: null,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function fakeSession(): SemiontSession {
|
|
39
|
+
return {
|
|
40
|
+
client: {
|
|
41
|
+
mark: {
|
|
42
|
+
changeSelection: vi.fn(), changeClick: vi.fn(), changeShape: vi.fn(),
|
|
43
|
+
toggleMode: vi.fn(), request: vi.fn(),
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
subscribe: () => () => {},
|
|
47
|
+
} as unknown as SemiontSession;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
type ViewProps = React.ComponentProps<typeof AnnotateView>;
|
|
51
|
+
|
|
52
|
+
const base = (mimeType: string): ViewProps => ({
|
|
53
|
+
resourceUri: 'res-1',
|
|
54
|
+
content: 'the-body',
|
|
55
|
+
mimeType,
|
|
56
|
+
annotations: emptyAnnotations,
|
|
57
|
+
uiState,
|
|
58
|
+
annotateMode: true,
|
|
59
|
+
session: fakeSession(),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('AnnotateView — media-renderer registry', () => {
|
|
63
|
+
it('uses a custom text renderer from the `renderers` override', () => {
|
|
64
|
+
const CustomText = ({ content }: AnnotateMediaRendererProps) => (
|
|
65
|
+
<div data-testid="custom-text">custom: {content}</div>
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
render(<AnnotateView {...base('text/plain')} renderers={{ text: CustomText }} />);
|
|
69
|
+
|
|
70
|
+
expect(screen.getByTestId('custom-text')).toHaveTextContent('custom: the-body');
|
|
71
|
+
expect(screen.queryByTestId('default-text')).not.toBeInTheDocument();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('uses a custom pdf renderer — the realistic override, a host bringing its own viewer', () => {
|
|
75
|
+
const CustomPdf = ({ content }: AnnotateMediaRendererProps) => (
|
|
76
|
+
<div data-testid="custom-pdf">{content}</div>
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
render(<AnnotateView {...base('application/pdf')} renderers={{ pdf: CustomPdf }} />);
|
|
80
|
+
|
|
81
|
+
expect(screen.getByTestId('custom-pdf')).toBeInTheDocument();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('merges over the defaults rather than replacing them: overriding text leaves image alone', () => {
|
|
85
|
+
const CustomText = () => <div data-testid="custom-text" />;
|
|
86
|
+
|
|
87
|
+
render(<AnnotateView {...base('image/png')} renderers={{ text: CustomText }} />);
|
|
88
|
+
|
|
89
|
+
expect(screen.getByTestId('default-image')).toBeInTheDocument();
|
|
90
|
+
expect(screen.queryByTestId('custom-text')).not.toBeInTheDocument();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('a mode in neither the defaults nor the override falls back to unsupported, download intact', () => {
|
|
94
|
+
// Deliberately unlike the browse side, where a registry miss falls through
|
|
95
|
+
// to the text renderer harmlessly. Annotating an unknown type is not
|
|
96
|
+
// harmless, so the miss must stay explicit.
|
|
97
|
+
const { container } = render(<AnnotateView {...base('application/octet-stream')} />);
|
|
98
|
+
|
|
99
|
+
expect(container.querySelector('[data-mime-type="unsupported"]')).toBeInTheDocument();
|
|
100
|
+
expect(screen.getByText(/Annotation not supported/)).toBeInTheDocument();
|
|
101
|
+
expect(screen.getByText('Download File')).toBeInTheDocument();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('renders one toolbar and one content shell, whatever the render mode', () => {
|
|
105
|
+
// The collapse this plan is really about: three branches used to repeat
|
|
106
|
+
// the wrapper, the toolbar block and the content div verbatim.
|
|
107
|
+
for (const mimeType of ['text/plain', 'application/pdf', 'image/png']) {
|
|
108
|
+
const { container, unmount } = render(<AnnotateView {...base(mimeType)} />);
|
|
109
|
+
expect(container.querySelectorAll('.semiont-annotate-view')).toHaveLength(1);
|
|
110
|
+
expect(container.querySelectorAll('.semiont-annotate-view__content')).toHaveLength(1);
|
|
111
|
+
unmount();
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -4,8 +4,6 @@ import { screen } from '@testing-library/react';
|
|
|
4
4
|
import '@testing-library/jest-dom';
|
|
5
5
|
import { AnnotationHistory } from '../AnnotationHistory';
|
|
6
6
|
import { renderWithProviders } from '../../../test-utils';
|
|
7
|
-
import type { ResourceId } from '@semiont/core';
|
|
8
|
-
import { BehaviorSubject } from 'rxjs';
|
|
9
7
|
|
|
10
8
|
// Mock @semiont/core - must use importOriginal to preserve EventBus etc.
|
|
11
9
|
vi.mock('@semiont/core', async (importOriginal) => {
|
|
@@ -22,33 +20,14 @@ vi.mock('../../../contexts/TranslationContext', () => ({
|
|
|
22
20
|
const translations: Record<string, string> = {
|
|
23
21
|
history: 'History',
|
|
24
22
|
loading: 'Loading...',
|
|
23
|
+
failed: 'Could not load history.',
|
|
24
|
+
retry: 'Try again',
|
|
25
25
|
};
|
|
26
26
|
return translations[key] || key;
|
|
27
27
|
}),
|
|
28
28
|
TranslationProvider: ({ children }: { children: React.ReactNode }) => children,
|
|
29
29
|
}));
|
|
30
30
|
|
|
31
|
-
const eventsSubject = new BehaviorSubject<any[] | undefined>(undefined);
|
|
32
|
-
const annotationsSubject = new BehaviorSubject<any[] | undefined>(undefined);
|
|
33
|
-
|
|
34
|
-
const stableMockClient = {
|
|
35
|
-
browse: {
|
|
36
|
-
events: () => eventsSubject.asObservable(),
|
|
37
|
-
annotations: () => annotationsSubject.asObservable(),
|
|
38
|
-
},
|
|
39
|
-
};
|
|
40
|
-
const stableMockSession = { client: stableMockClient };
|
|
41
|
-
const stableActiveSession$ = new BehaviorSubject<any>(stableMockSession);
|
|
42
|
-
const stableMockBrowser = { activeSession$: stableActiveSession$ };
|
|
43
|
-
|
|
44
|
-
vi.mock('../../../session/SemiontProvider', async (importOriginal) => {
|
|
45
|
-
const actual = await importOriginal<typeof import('../../../session/SemiontProvider')>();
|
|
46
|
-
return {
|
|
47
|
-
...actual,
|
|
48
|
-
useSemiont: () => stableMockBrowser,
|
|
49
|
-
};
|
|
50
|
-
});
|
|
51
|
-
|
|
52
31
|
// Mock HistoryEvent to avoid deep rendering and mocking all its dependencies
|
|
53
32
|
const MockHistoryEvent = vi.fn(({ event }: any) => (
|
|
54
33
|
<div data-testid={`history-event-${event.id}`}>
|
|
@@ -63,8 +42,6 @@ vi.mock('../HistoryEvent', () => ({
|
|
|
63
42
|
import { getAnnotationUriFromEvent } from '@semiont/core';
|
|
64
43
|
const mockGetAnnotationUri = getAnnotationUriFromEvent as ReturnType<typeof vi.fn>;
|
|
65
44
|
|
|
66
|
-
const testRId = 'res-1' as ResourceId;
|
|
67
|
-
|
|
68
45
|
/** Returns flat StoredEventResponse shape (matches API response) */
|
|
69
46
|
function makeStoredEvent(id: string, type: string, seq: number, overrides: Record<string, any> = {}): any {
|
|
70
47
|
return {
|
|
@@ -91,16 +68,13 @@ describe('AnnotationHistory', () => {
|
|
|
91
68
|
beforeEach(() => {
|
|
92
69
|
vi.clearAllMocks();
|
|
93
70
|
mockGetAnnotationUri.mockReturnValue(null);
|
|
94
|
-
eventsSubject.next(undefined);
|
|
95
|
-
annotationsSubject.next([]);
|
|
96
71
|
});
|
|
97
72
|
|
|
98
73
|
it('renders loading state', () => {
|
|
99
|
-
eventsSubject.next(undefined);
|
|
100
|
-
|
|
101
74
|
renderWithProviders(
|
|
102
75
|
<AnnotationHistory
|
|
103
|
-
|
|
76
|
+
events={[]}
|
|
77
|
+
eventsLoading
|
|
104
78
|
Link={MockLink}
|
|
105
79
|
routes={mockRoutes}
|
|
106
80
|
/>
|
|
@@ -110,12 +84,64 @@ describe('AnnotationHistory', () => {
|
|
|
110
84
|
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
|
111
85
|
});
|
|
112
86
|
|
|
113
|
-
|
|
114
|
-
|
|
87
|
+
describe('terminal load failure', () => {
|
|
88
|
+
// The panel used to derive `loading` from `eventsData === undefined` and
|
|
89
|
+
// hard-code `const error = false`, so a failed load sat on "Loading..."
|
|
90
|
+
// for ever and the error branch below it was unreachable.
|
|
91
|
+
// See .plans/PANEL-FAILURE-STATES.md
|
|
92
|
+
|
|
93
|
+
it('reports the failure instead of staying on the loading text', () => {
|
|
94
|
+
renderWithProviders(
|
|
95
|
+
<AnnotationHistory
|
|
96
|
+
events={[]}
|
|
97
|
+
eventsLoading={false}
|
|
98
|
+
eventsError={new Error('Resource not found')}
|
|
99
|
+
Link={MockLink}
|
|
100
|
+
routes={mockRoutes}
|
|
101
|
+
/>
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
|
105
|
+
expect(screen.getByText('Could not load history.')).toBeInTheDocument();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('offers a retry that calls back', async () => {
|
|
109
|
+
const onRetryEvents = vi.fn();
|
|
110
|
+
const { default: userEvent } = await import('@testing-library/user-event');
|
|
111
|
+
|
|
112
|
+
renderWithProviders(
|
|
113
|
+
<AnnotationHistory
|
|
114
|
+
events={[]}
|
|
115
|
+
eventsError={new Error('boom')}
|
|
116
|
+
onRetryEvents={onRetryEvents}
|
|
117
|
+
Link={MockLink}
|
|
118
|
+
routes={mockRoutes}
|
|
119
|
+
/>
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
await userEvent.click(screen.getByRole('button', { name: 'Try again' }));
|
|
123
|
+
expect(onRetryEvents).toHaveBeenCalledTimes(1);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('the failure beats the loading state — a dead request is not still loading', () => {
|
|
127
|
+
renderWithProviders(
|
|
128
|
+
<AnnotationHistory
|
|
129
|
+
events={[]}
|
|
130
|
+
eventsLoading
|
|
131
|
+
eventsError={new Error('boom')}
|
|
132
|
+
Link={MockLink}
|
|
133
|
+
routes={mockRoutes}
|
|
134
|
+
/>
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
expect(screen.getByText('Could not load history.')).toBeInTheDocument();
|
|
138
|
+
});
|
|
139
|
+
});
|
|
115
140
|
|
|
141
|
+
it('renders null when no events', () => {
|
|
116
142
|
const { container } = renderWithProviders(
|
|
117
143
|
<AnnotationHistory
|
|
118
|
-
|
|
144
|
+
events={[]}
|
|
119
145
|
Link={MockLink}
|
|
120
146
|
routes={mockRoutes}
|
|
121
147
|
/>
|
|
@@ -130,11 +156,10 @@ describe('AnnotationHistory', () => {
|
|
|
130
156
|
makeStoredEvent('e1', 'mark:added', 1),
|
|
131
157
|
makeStoredEvent('e2', 'mark:added', 2),
|
|
132
158
|
];
|
|
133
|
-
eventsSubject.next(events);
|
|
134
159
|
|
|
135
160
|
renderWithProviders(
|
|
136
161
|
<AnnotationHistory
|
|
137
|
-
|
|
162
|
+
events={events}
|
|
138
163
|
Link={MockLink}
|
|
139
164
|
routes={mockRoutes}
|
|
140
165
|
/>
|
|
@@ -155,11 +180,10 @@ describe('AnnotationHistory', () => {
|
|
|
155
180
|
makeStoredEvent('e4', 'job:completed', 4),
|
|
156
181
|
makeStoredEvent('e5', 'mark:body-updated', 5),
|
|
157
182
|
];
|
|
158
|
-
eventsSubject.next(events);
|
|
159
183
|
|
|
160
184
|
renderWithProviders(
|
|
161
185
|
<AnnotationHistory
|
|
162
|
-
|
|
186
|
+
events={events}
|
|
163
187
|
Link={MockLink}
|
|
164
188
|
routes={mockRoutes}
|
|
165
189
|
/>
|
|
@@ -173,12 +197,11 @@ describe('AnnotationHistory', () => {
|
|
|
173
197
|
|
|
174
198
|
it('passes isRelated when hovered annotation matches event', () => {
|
|
175
199
|
const events = [makeStoredEvent('e1', 'mark:added', 1)];
|
|
176
|
-
eventsSubject.next(events);
|
|
177
200
|
mockGetAnnotationUri.mockReturnValue('ann-1');
|
|
178
201
|
|
|
179
202
|
renderWithProviders(
|
|
180
203
|
<AnnotationHistory
|
|
181
|
-
|
|
204
|
+
events={events}
|
|
182
205
|
hoveredAnnotationId="ann-1"
|
|
183
206
|
Link={MockLink}
|
|
184
207
|
routes={mockRoutes}
|
|
@@ -192,11 +215,10 @@ describe('AnnotationHistory', () => {
|
|
|
192
215
|
|
|
193
216
|
it('renders history panel structure with title and list', () => {
|
|
194
217
|
const events = [makeStoredEvent('e1', 'mark:added', 1)];
|
|
195
|
-
eventsSubject.next(events);
|
|
196
218
|
|
|
197
219
|
const { container } = renderWithProviders(
|
|
198
220
|
<AnnotationHistory
|
|
199
|
-
|
|
221
|
+
events={events}
|
|
200
222
|
Link={MockLink}
|
|
201
223
|
routes={mockRoutes}
|
|
202
224
|
/>
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* `browse.resource` observable so the test controls resolve timing.
|
|
14
14
|
*/
|
|
15
15
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
16
|
+
import { asStates } from '../../../__tests__/test-client';
|
|
16
17
|
import { render, fireEvent } from '@testing-library/react';
|
|
17
18
|
import '@testing-library/jest-dom';
|
|
18
19
|
import { BehaviorSubject } from 'rxjs';
|
|
@@ -53,7 +54,7 @@ const stubRef = makeAnnotation('ref-stub', []);
|
|
|
53
54
|
function setup(onReferenceHover?: (h: unknown) => void) {
|
|
54
55
|
const referent$ = new BehaviorSubject<ResourceDescriptor | undefined>(undefined);
|
|
55
56
|
const beckonHover = vi.fn();
|
|
56
|
-
const browseResource = vi.fn(() => referent$);
|
|
57
|
+
const browseResource = vi.fn(() => asStates(referent$));
|
|
57
58
|
const session = {
|
|
58
59
|
client: {
|
|
59
60
|
browse: { click: vi.fn(), resource: browseResource },
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MULTI-RESOURCE-SCOPE Step 5 — the multi-mount invalidation contract.
|
|
3
|
+
*
|
|
4
|
+
* With N viewers mounted on one session (the embeddable "resource per chat
|
|
5
|
+
* message" pattern — 40–60 concurrent in the surveyed consumer), a mark
|
|
6
|
+
* event must cost ONE invalidation, of the right resource's list — not N.
|
|
7
|
+
*
|
|
8
|
+
* The invalidation belongs to the sdk: `BrowseNamespace.subscribeToEvents`
|
|
9
|
+
* already invalidates payload-keyed on mark:added/mark:removed and patches
|
|
10
|
+
* mark:body-updated in place. `ResourceViewer` therefore subscribes to NO
|
|
11
|
+
* mark:* channels at all — a viewer-side handler was pure duplication, and
|
|
12
|
+
* unfiltered it was an O(N) refetch amplification. This suite pins the
|
|
13
|
+
* system behavior over the real client with two mounted viewers:
|
|
14
|
+
* exactly one list invalidation per mark event, zero for body updates.
|
|
15
|
+
*/
|
|
16
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
17
|
+
import { render, act, waitFor } from '@testing-library/react';
|
|
18
|
+
import '@testing-library/jest-dom';
|
|
19
|
+
import type { ResourceDescriptor as SemiontResource, ResourceId, StoredEvent, EventOfType, UserId, EventMetadata, Annotation } from '@semiont/core';
|
|
20
|
+
import { annotationId, resourceId as makeResourceId } from '@semiont/core';
|
|
21
|
+
import { createTestSemiontWrapper } from '../../../test-utils';
|
|
22
|
+
import { ResourceViewer } from '../ResourceViewer';
|
|
23
|
+
|
|
24
|
+
const TEST_USER = 'did:web:test:users:test' as UserId;
|
|
25
|
+
|
|
26
|
+
function makeResource(id: string): SemiontResource & { content: string } {
|
|
27
|
+
return {
|
|
28
|
+
'@context': 'https://www.w3.org/ns/activitystreams',
|
|
29
|
+
'@id': id as ResourceId,
|
|
30
|
+
name: `Doc ${id}`,
|
|
31
|
+
created: '2024-01-01T00:00:00Z',
|
|
32
|
+
entityTypes: [],
|
|
33
|
+
archived: false,
|
|
34
|
+
representations: [{ mediaType: 'text/plain', byteSize: 10 }],
|
|
35
|
+
content: `Content of ${id}.`,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function mockAnnotation(rIdStr: string): Annotation {
|
|
40
|
+
return {
|
|
41
|
+
'@context': 'http://www.w3.org/ns/anno.jsonld',
|
|
42
|
+
type: 'Annotation',
|
|
43
|
+
id: annotationId('ann-1'),
|
|
44
|
+
motivation: 'commenting',
|
|
45
|
+
created: '2026-01-01T00:00:00Z',
|
|
46
|
+
target: { source: rIdStr },
|
|
47
|
+
body: [{ type: 'TextualBody', value: 'c', purpose: 'commenting' }],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function fakeMarkAdded(rIdStr: string): StoredEvent<EventOfType<'mark:added'>> {
|
|
52
|
+
return {
|
|
53
|
+
id: 'evt-1',
|
|
54
|
+
type: 'mark:added',
|
|
55
|
+
resourceId: makeResourceId(rIdStr),
|
|
56
|
+
userId: TEST_USER,
|
|
57
|
+
version: 1,
|
|
58
|
+
timestamp: '2026-01-01T00:00:00Z',
|
|
59
|
+
payload: { annotation: mockAnnotation(rIdStr) },
|
|
60
|
+
metadata: { sequenceNumber: 1 } as EventMetadata,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const annotations = { highlights: [], references: [], assessments: [], comments: [], tags: [] };
|
|
65
|
+
|
|
66
|
+
function mountTwoViewers() {
|
|
67
|
+
const wrapper = createTestSemiontWrapper();
|
|
68
|
+
const spy = vi.spyOn(wrapper.client.browse, 'invalidateAnnotationList');
|
|
69
|
+
render(
|
|
70
|
+
<>
|
|
71
|
+
<ResourceViewer
|
|
72
|
+
session={wrapper.session}
|
|
73
|
+
resource={makeResource('res-A')}
|
|
74
|
+
annotations={annotations}
|
|
75
|
+
onOpenResource={vi.fn()}
|
|
76
|
+
onOpenPanel={vi.fn()}
|
|
77
|
+
/>
|
|
78
|
+
<ResourceViewer
|
|
79
|
+
session={wrapper.session}
|
|
80
|
+
resource={makeResource('res-B')}
|
|
81
|
+
annotations={annotations}
|
|
82
|
+
onOpenResource={vi.fn()}
|
|
83
|
+
onOpenPanel={vi.fn()}
|
|
84
|
+
/>
|
|
85
|
+
</>,
|
|
86
|
+
);
|
|
87
|
+
spy.mockClear(); // only the event-driven invalidations count
|
|
88
|
+
return { ...wrapper, spy };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const flush = () => act(() => new Promise<void>((r) => setTimeout(r, 0)));
|
|
92
|
+
|
|
93
|
+
describe('ResourceViewer — multi-mount invalidation is per-resource and O(1), not O(N)', () => {
|
|
94
|
+
it("a mark:added for res-A costs exactly ONE invalidation — res-A's, from the sdk", async () => {
|
|
95
|
+
const { eventBus, spy } = mountTwoViewers();
|
|
96
|
+
|
|
97
|
+
act(() => {
|
|
98
|
+
eventBus.get('mark:added').next(fakeMarkAdded('res-A'));
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
await waitFor(() => expect(spy).toHaveBeenCalledWith('res-A'));
|
|
102
|
+
expect(spy).not.toHaveBeenCalledWith('res-B');
|
|
103
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("a mark:removed for res-B costs exactly ONE invalidation — res-B's", async () => {
|
|
107
|
+
const { eventBus, spy } = mountTwoViewers();
|
|
108
|
+
|
|
109
|
+
act(() => {
|
|
110
|
+
eventBus.get('mark:removed').next({
|
|
111
|
+
id: 'evt-2',
|
|
112
|
+
type: 'mark:removed',
|
|
113
|
+
resourceId: makeResourceId('res-B'),
|
|
114
|
+
userId: TEST_USER,
|
|
115
|
+
version: 1,
|
|
116
|
+
timestamp: '2026-01-01T00:00:00Z',
|
|
117
|
+
payload: { annotationId: annotationId('ann-1') },
|
|
118
|
+
metadata: { sequenceNumber: 2 } as EventMetadata,
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
await waitFor(() => expect(spy).toHaveBeenCalledWith('res-B'));
|
|
123
|
+
expect(spy).not.toHaveBeenCalledWith('res-A');
|
|
124
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('a mark:body-updated costs ZERO list invalidations — the sdk patches in place', async () => {
|
|
128
|
+
const { eventBus, spy } = mountTwoViewers();
|
|
129
|
+
|
|
130
|
+
act(() => {
|
|
131
|
+
eventBus.get('mark:body-updated').next({
|
|
132
|
+
resourceId: makeResourceId('res-A'),
|
|
133
|
+
annotation: mockAnnotation('res-A'),
|
|
134
|
+
} as never);
|
|
135
|
+
});
|
|
136
|
+
await flush();
|
|
137
|
+
|
|
138
|
+
expect(spy).not.toHaveBeenCalled();
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The media-renderer registries must be reachable through `ResourceViewer` —
|
|
3
|
+
* the documented consumer entry point.
|
|
4
|
+
*
|
|
5
|
+
* `BrowseView` has taken a `renderers` override since EMBEDDABLE-RESOURCE-VIEWER
|
|
6
|
+
* step 3, and `AnnotateView` since 3b, but `ResourceViewer` forwarded neither.
|
|
7
|
+
* A host importing it (what docs/ANNOTATIONS.md tells consumers to do, and what
|
|
8
|
+
* the embeddable-surface packaging gate checks) therefore had no way to reach
|
|
9
|
+
* the extension point: it would have to drop to `BrowseView` directly and
|
|
10
|
+
* reimplement the browse/annotate switching `ResourceViewer` exists to provide.
|
|
11
|
+
*
|
|
12
|
+
* Started RED (tsc: no `browseRenderers` / `annotateRenderers` props).
|
|
13
|
+
* See .plans/ANNOTATE-RENDERER-REGISTRY.md (D5)
|
|
14
|
+
*/
|
|
15
|
+
import React from 'react';
|
|
16
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
17
|
+
import { render, screen } from '@testing-library/react';
|
|
18
|
+
import '@testing-library/jest-dom';
|
|
19
|
+
import type { SemiontSession } from '@semiont/sdk';
|
|
20
|
+
import { ResourceViewer } from '../ResourceViewer';
|
|
21
|
+
import type { MediaRendererProps } from '../browse-renderers';
|
|
22
|
+
import type { AnnotateMediaRendererProps } from '../annotate-renderers';
|
|
23
|
+
|
|
24
|
+
vi.mock('../../annotation/AnnotateToolbar', () => ({ AnnotateToolbar: () => null }));
|
|
25
|
+
vi.mock('../../Toolbar', () => ({ Toolbar: () => null }));
|
|
26
|
+
vi.mock('../../CodeMirrorRenderer', () => ({
|
|
27
|
+
CodeMirrorRenderer: () => <div data-testid="default-text">default-cm</div>,
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
function fakeSession(): SemiontSession {
|
|
31
|
+
return {
|
|
32
|
+
client: {
|
|
33
|
+
browse: { click: vi.fn() },
|
|
34
|
+
beckon: { hover: vi.fn() },
|
|
35
|
+
mark: {
|
|
36
|
+
changeSelection: vi.fn(), changeClick: vi.fn(), changeShape: vi.fn(),
|
|
37
|
+
toggleMode: vi.fn(), request: vi.fn(),
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
subscribe: () => () => {},
|
|
41
|
+
} as unknown as SemiontSession;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type ViewerProps = React.ComponentProps<typeof ResourceViewer>;
|
|
45
|
+
|
|
46
|
+
const emptyAnnotations = { highlights: [], references: [], assessments: [], comments: [], tags: [] };
|
|
47
|
+
|
|
48
|
+
const base = (): ViewerProps => ({
|
|
49
|
+
resource: {
|
|
50
|
+
'@id': 'res-1',
|
|
51
|
+
name: 'Doc',
|
|
52
|
+
content: 'the-body',
|
|
53
|
+
representations: [{ mediaType: 'text/plain' }],
|
|
54
|
+
},
|
|
55
|
+
annotations: emptyAnnotations,
|
|
56
|
+
session: fakeSession(),
|
|
57
|
+
} as unknown as ViewerProps);
|
|
58
|
+
|
|
59
|
+
describe('ResourceViewer — media-renderer registries are reachable', () => {
|
|
60
|
+
it('forwards browseRenderers to the read-only path', () => {
|
|
61
|
+
const CustomText = ({ content }: MediaRendererProps) => (
|
|
62
|
+
<div data-testid="custom-browse">{content}</div>
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
render(<ResourceViewer {...base()} annotateMode={false} browseRenderers={{ text: CustomText }} />);
|
|
66
|
+
|
|
67
|
+
expect(screen.getByTestId('custom-browse')).toHaveTextContent('the-body');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('forwards annotateRenderers to the annotating path', () => {
|
|
71
|
+
const CustomText = ({ content }: AnnotateMediaRendererProps) => (
|
|
72
|
+
<div data-testid="custom-annotate">{content}</div>
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
render(<ResourceViewer {...base()} annotateMode annotateRenderers={{ text: CustomText }} />);
|
|
76
|
+
|
|
77
|
+
expect(screen.getByTestId('custom-annotate')).toHaveTextContent('the-body');
|
|
78
|
+
expect(screen.queryByTestId('default-text')).not.toBeInTheDocument();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('without overrides the defaults still render, in both modes', () => {
|
|
82
|
+
const { unmount } = render(<ResourceViewer {...base()} annotateMode />);
|
|
83
|
+
expect(screen.getByTestId('default-text')).toBeInTheDocument();
|
|
84
|
+
unmount();
|
|
85
|
+
|
|
86
|
+
render(<ResourceViewer {...base()} annotateMode={false} />);
|
|
87
|
+
expect(screen.getByText(/the-body/)).toBeInTheDocument();
|
|
88
|
+
});
|
|
89
|
+
});
|