@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.
- package/README.md +1 -1
- package/dist/{PdfAnnotationCanvas.client-P37SXBWQ.js → PdfAnnotationCanvas.client-75GY2EDR.js} +15 -6
- package/dist/PdfAnnotationCanvas.client-75GY2EDR.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/{chunk-O2MD7TGE.js → chunk-VXASXU4K.js} +10 -2
- package/dist/chunk-VXASXU4K.js.map +1 -0
- package/dist/index.css +150 -112
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +334 -48
- package/dist/index.js +2494 -2201
- package/dist/index.js.map +1 -1
- package/dist/test-utils.d.ts +5 -1
- package/dist/test-utils.js +10 -4
- package/dist/test-utils.js.map +1 -1
- package/package.json +7 -7
- 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/AnnotationOverlay.tsx +6 -6
- package/src/components/image-annotation/SvgDrawingCanvas.tsx +17 -5
- package/src/components/image-annotation/__tests__/AnnotationOverlay.click.test.tsx +137 -0
- package/src/components/image-annotation/__tests__/SvgDrawingCanvas.click.test.tsx +108 -0
- package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +17 -4
- package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +143 -7
- package/src/components/resource/AnnotateView.tsx +59 -38
- package/src/components/resource/BrowseView.tsx +172 -138
- package/src/components/resource/ResourceViewer.tsx +123 -130
- 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 +133 -53
- package/src/components/resource/__tests__/ResourceViewer.embeddable.test.tsx +102 -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.dispatch.test.tsx +113 -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 +85 -0
- package/src/components/resource/panels/AssessmentEntry.tsx +5 -4
- package/src/components/resource/panels/AssessmentPanel.tsx +12 -5
- package/src/components/resource/panels/AssistSection.tsx +4 -3
- package/src/components/resource/panels/CommentEntry.tsx +5 -4
- package/src/components/resource/panels/CommentsPanel.tsx +12 -5
- package/src/components/resource/panels/HighlightEntry.tsx +5 -4
- package/src/components/resource/panels/HighlightPanel.tsx +12 -5
- package/src/components/resource/panels/ReferenceEntry.tsx +9 -10
- package/src/components/resource/panels/ReferencesPanel.tsx +15 -6
- package/src/components/resource/panels/ResourceInfoPanel.tsx +4 -3
- package/src/components/resource/panels/TagEntry.tsx +5 -3
- package/src/components/resource/panels/TaggingPanel.tsx +11 -4
- package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +15 -2
- package/src/components/resource/panels/__tests__/AssessmentEntry.test.tsx +28 -20
- package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +39 -28
- package/src/components/resource/panels/__tests__/AssistSection.test.tsx +46 -17
- package/src/components/resource/panels/__tests__/CommentEntry.test.tsx +65 -57
- package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +54 -24
- package/src/components/resource/panels/__tests__/HighlightEntry.test.tsx +27 -17
- package/src/components/resource/panels/__tests__/HighlightPanel.annotationProgress.test.tsx +23 -17
- package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +48 -77
- package/src/components/resource/panels/__tests__/ReferencesPanel.headless.test.tsx +87 -0
- package/src/components/resource/panels/__tests__/ReferencesPanel.observable-flow.test.tsx +15 -9
- package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +78 -59
- package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +11 -4
- package/src/components/resource/panels/__tests__/TagEntry.test.tsx +33 -19
- package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +16 -7
- 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 +43 -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
- package/dist/chunk-O2MD7TGE.js.map +0 -1
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TOOLBAR-PREFS-AS-PROPS — keystone + per-pref specs.
|
|
3
|
+
*
|
|
4
|
+
* Preferences are state, not events: each of the four toolbar prefs (mode, click
|
|
5
|
+
* action, selection motivation, shape) is a controlled/uncontrolled prop pair.
|
|
6
|
+
* Controlled instances render the given value and report intents via the callback —
|
|
7
|
+
* never self-mutate, never touch localStorage, never hear other instances.
|
|
8
|
+
* Uncontrolled instances hold a plain internal default (false / 'detail' /
|
|
9
|
+
* 'linking' / 'rectangle') — NOT the legacy localStorage+bus behavior (that lives
|
|
10
|
+
* in the useToolbarPrefs() policy layer).
|
|
11
|
+
*
|
|
12
|
+
* RED ledger: authored `it.fails` (observed: 5 expected fail), flipped to `it` at
|
|
13
|
+
* Phase 1 GREEN. Provider-free; real AnnotateToolbar (its controls are the subject).
|
|
14
|
+
*/
|
|
15
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
16
|
+
import { render, fireEvent, within, waitFor } from '@testing-library/react';
|
|
17
|
+
import '@testing-library/jest-dom';
|
|
18
|
+
import type { SemiontSession } from '@semiont/sdk';
|
|
19
|
+
import type { ResourceDescriptor as SemiontResource, ResourceId } from '@semiont/core';
|
|
20
|
+
import { ResourceViewer } from '../ResourceViewer';
|
|
21
|
+
|
|
22
|
+
vi.mock('../../CodeMirrorRenderer', () => ({ CodeMirrorRenderer: () => <div>cm-mock</div> }));
|
|
23
|
+
vi.mock('../../image-annotation/SvgDrawingCanvas', () => ({ SvgDrawingCanvas: () => <div>svg-mock</div> }));
|
|
24
|
+
vi.mock('react-markdown', () => ({ default: ({ children }: { children: string }) => <div>{children}</div> }));
|
|
25
|
+
vi.mock('remark-gfm', () => ({ default: () => ({}) }));
|
|
26
|
+
|
|
27
|
+
function makeResource(mediaType = 'text/plain'): SemiontResource & { content: string } {
|
|
28
|
+
return {
|
|
29
|
+
'@context': 'https://www.w3.org/ns/activitystreams',
|
|
30
|
+
'@id': 'res-1' as ResourceId,
|
|
31
|
+
name: 'Doc',
|
|
32
|
+
created: '2024-01-01T00:00:00Z',
|
|
33
|
+
entityTypes: [],
|
|
34
|
+
archived: false,
|
|
35
|
+
representations: [{ mediaType, byteSize: 10 }],
|
|
36
|
+
content: 'Prefs content.',
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const annotations = { highlights: [], references: [], assessments: [], comments: [], tags: [] };
|
|
41
|
+
|
|
42
|
+
/** Session double with a live subscribe registry so specs can fire legacy broadcasts. */
|
|
43
|
+
function liveSession() {
|
|
44
|
+
const handlers = new Map<string, Set<(p: unknown) => void>>();
|
|
45
|
+
const client = {
|
|
46
|
+
browse: { click: vi.fn(), invalidateAnnotationList: vi.fn() },
|
|
47
|
+
beckon: { hover: vi.fn() },
|
|
48
|
+
mark: {
|
|
49
|
+
toggleMode: vi.fn(), delete: vi.fn(), request: vi.fn(),
|
|
50
|
+
changeSelection: vi.fn(), changeClick: vi.fn(), changeShape: vi.fn(),
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
const session = {
|
|
54
|
+
client,
|
|
55
|
+
subscribe: (channel: string, handler: (p: unknown) => void) => {
|
|
56
|
+
if (!handlers.has(channel)) handlers.set(channel, new Set());
|
|
57
|
+
handlers.get(channel)!.add(handler);
|
|
58
|
+
return () => { handlers.get(channel)!.delete(handler); };
|
|
59
|
+
},
|
|
60
|
+
} as unknown as SemiontSession;
|
|
61
|
+
const fire = (channel: string, payload?: unknown) =>
|
|
62
|
+
handlers.get(channel)?.forEach((h) => h(payload));
|
|
63
|
+
return { session, client, fire };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const isAnnotate = (c: HTMLElement) => !!c.querySelector('.semiont-annotate-view');
|
|
67
|
+
const isBrowse = (c: HTMLElement) => !!c.querySelector('.semiont-browse-view');
|
|
68
|
+
|
|
69
|
+
/** Open a bar dropdown group by aria-label and click an option in its menu. */
|
|
70
|
+
async function pick(container: HTMLElement, group: string, option: string) {
|
|
71
|
+
const trigger = within(container).getByLabelText(group);
|
|
72
|
+
fireEvent.mouseEnter(trigger.parentElement!);
|
|
73
|
+
await waitFor(() => {
|
|
74
|
+
const menu = within(container).getByRole('menu');
|
|
75
|
+
fireEvent.click(within(menu).getByText(option));
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const PREF_KEYS = ['annotateMode', 'semiont-toolbar-click', 'semiont-toolbar-selection'];
|
|
80
|
+
let getSpy: ReturnType<typeof vi.spyOn>;
|
|
81
|
+
let setSpy: ReturnType<typeof vi.spyOn>;
|
|
82
|
+
const prefKeyTouched = (spy: ReturnType<typeof vi.spyOn>) =>
|
|
83
|
+
spy.mock.calls.some((call: unknown[]) => {
|
|
84
|
+
const key = String(call[0]);
|
|
85
|
+
return PREF_KEYS.includes(key) || key.includes('shape');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('ResourceViewer — toolbar prefs as props', () => {
|
|
89
|
+
beforeEach(() => {
|
|
90
|
+
vi.clearAllMocks();
|
|
91
|
+
localStorage.clear();
|
|
92
|
+
getSpy = vi.spyOn(Storage.prototype, 'getItem');
|
|
93
|
+
setSpy = vi.spyOn(Storage.prototype, 'setItem');
|
|
94
|
+
});
|
|
95
|
+
afterEach(() => {
|
|
96
|
+
getSpy.mockRestore();
|
|
97
|
+
setSpy.mockRestore();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// ── Keystone: per-instance isolation across three viewers on one session ──
|
|
101
|
+
it('keystone: three viewers — controlled A/B independent, C uncontrolled; B\'s Mode click fires only B\'s callback', async () => {
|
|
102
|
+
const { session, client } = liveSession();
|
|
103
|
+
const onB = vi.fn();
|
|
104
|
+
const a = render(
|
|
105
|
+
<ResourceViewer session={session} resource={makeResource()} annotations={annotations}
|
|
106
|
+
annotateMode={true} clickAction="follow" onAnnotateModeChange={vi.fn()} onClickActionChange={vi.fn()} />,
|
|
107
|
+
);
|
|
108
|
+
const b = render(
|
|
109
|
+
<ResourceViewer session={session} resource={makeResource()} annotations={annotations}
|
|
110
|
+
annotateMode={false} onAnnotateModeChange={onB} />,
|
|
111
|
+
);
|
|
112
|
+
const c = render(
|
|
113
|
+
<ResourceViewer session={session} resource={makeResource()} annotations={annotations} />,
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
expect(isAnnotate(a.container)).toBe(true); // A renders its controlled mode
|
|
117
|
+
expect(isBrowse(b.container)).toBe(true);
|
|
118
|
+
expect(isBrowse(c.container)).toBe(true); // C: plain default (false)
|
|
119
|
+
|
|
120
|
+
await pick(b.container, 'Mode', 'Annotate');
|
|
121
|
+
|
|
122
|
+
expect(onB).toHaveBeenCalledWith(true); // B reports intent…
|
|
123
|
+
expect(isBrowse(b.container)).toBe(true); // …and does not self-flip
|
|
124
|
+
expect(isAnnotate(a.container)).toBe(true); // A unchanged
|
|
125
|
+
expect(isBrowse(c.container)).toBe(true); // C unchanged
|
|
126
|
+
expect(client.mark.toggleMode).not.toHaveBeenCalled(); // no global emit
|
|
127
|
+
expect(prefKeyTouched(getSpy)).toBe(false); // no localStorage traffic
|
|
128
|
+
expect(prefKeyTouched(setSpy)).toBe(false);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// ── Per-pref: mode ──
|
|
132
|
+
it('mode: uncontrolled uses the plain default and is inert to the legacy broadcast', () => {
|
|
133
|
+
localStorage.setItem('annotateMode', 'true'); // legacy key must be IGNORED
|
|
134
|
+
getSpy.mockClear(); setSpy.mockClear();
|
|
135
|
+
const { session, fire } = liveSession();
|
|
136
|
+
const u = render(
|
|
137
|
+
<ResourceViewer session={session} resource={makeResource()} annotations={annotations} />,
|
|
138
|
+
);
|
|
139
|
+
expect(isBrowse(u.container)).toBe(true); // plain default, not the stored 'true'
|
|
140
|
+
fire('mark:mode-toggled');
|
|
141
|
+
expect(isBrowse(u.container)).toBe(true); // inert: preference events are gone
|
|
142
|
+
expect(prefKeyTouched(getSpy)).toBe(false);
|
|
143
|
+
expect(prefKeyTouched(setSpy)).toBe(false);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// ── Per-pref: click action ──
|
|
147
|
+
it('clickAction: controlled renders the value; the bar control reports and does not mutate', async () => {
|
|
148
|
+
const { session, client } = liveSession();
|
|
149
|
+
const onChange = vi.fn();
|
|
150
|
+
const v = render(
|
|
151
|
+
<ResourceViewer session={session} resource={makeResource()} annotations={annotations}
|
|
152
|
+
clickAction="follow" onClickActionChange={onChange} />,
|
|
153
|
+
);
|
|
154
|
+
const bar = v.container.querySelector('.semiont-annotate-toolbar') as HTMLElement;
|
|
155
|
+
expect(within(bar).getByText('Follow')).toBeInTheDocument(); // renders the prop value
|
|
156
|
+
|
|
157
|
+
await pick(v.container, 'Click', 'Detail');
|
|
158
|
+
expect(onChange).toHaveBeenCalledWith('detail'); // reports intent
|
|
159
|
+
expect(within(bar).getByText('Follow')).toBeInTheDocument(); // does not self-mutate
|
|
160
|
+
expect(client.mark.changeClick).not.toHaveBeenCalled();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// ── Per-pref: selection motivation (annotate-mode bar) ──
|
|
164
|
+
it('selectionMotivation: controlled renders the value; picking reports; re-picking the current reports null', async () => {
|
|
165
|
+
const { session, client } = liveSession();
|
|
166
|
+
const onChange = vi.fn();
|
|
167
|
+
const v = render(
|
|
168
|
+
<ResourceViewer session={session} resource={makeResource()} annotations={annotations}
|
|
169
|
+
annotateMode={true} onAnnotateModeChange={vi.fn()}
|
|
170
|
+
selectionMotivation="highlighting" onSelectionMotivationChange={onChange} />,
|
|
171
|
+
);
|
|
172
|
+
const bar = v.container.querySelector('.semiont-annotate-toolbar') as HTMLElement;
|
|
173
|
+
expect(within(bar).getByText('Highlight')).toBeInTheDocument(); // renders the prop value
|
|
174
|
+
|
|
175
|
+
await pick(v.container, 'Motivation', 'Comment');
|
|
176
|
+
expect(onChange).toHaveBeenCalledWith('commenting');
|
|
177
|
+
|
|
178
|
+
await pick(v.container, 'Motivation', 'Highlight'); // current value → toggles off
|
|
179
|
+
expect(onChange).toHaveBeenCalledWith(null);
|
|
180
|
+
expect(client.mark.changeSelection).not.toHaveBeenCalled();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// ── Per-pref: shape (media-gated; image annotate bar) ──
|
|
184
|
+
it('shape: controlled renders the value; picking reports and does not mutate', async () => {
|
|
185
|
+
const { session, client } = liveSession();
|
|
186
|
+
const onChange = vi.fn();
|
|
187
|
+
const v = render(
|
|
188
|
+
<ResourceViewer session={session} resource={makeResource('image/png')} annotations={annotations}
|
|
189
|
+
annotateMode={true} onAnnotateModeChange={vi.fn()}
|
|
190
|
+
shape="circle" onShapeChange={onChange} />,
|
|
191
|
+
);
|
|
192
|
+
const bar = v.container.querySelector('.semiont-annotate-toolbar') as HTMLElement;
|
|
193
|
+
expect(within(bar).getByText('Circle')).toBeInTheDocument(); // renders the prop value
|
|
194
|
+
|
|
195
|
+
await pick(v.container, 'Shape', 'Polygon');
|
|
196
|
+
expect(onChange).toHaveBeenCalledWith('polygon');
|
|
197
|
+
expect(within(bar).getByText('Circle')).toBeInTheDocument(); // does not self-mutate
|
|
198
|
+
expect(client.mark.changeShape).not.toHaveBeenCalled();
|
|
199
|
+
});
|
|
200
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BUG: image-browse-renderer-drops-annotations — the default renderers must
|
|
3
|
+
* forward what BrowseView hands them.
|
|
4
|
+
*
|
|
5
|
+
* ImageBrowseRenderer destructured only content/mimeType and mounted a bare
|
|
6
|
+
* ImageViewer: shape annotations invisible in browse mode (annotate mode
|
|
7
|
+
* proves the data). Fix: the read-only annotation canvas (drawingMode=null),
|
|
8
|
+
* WITH the session extension on MediaRendererProps — clicks/hover route in
|
|
9
|
+
* browse mode too, and the PDF renderer's pre-existing session-less click gap
|
|
10
|
+
* heals in the same motion (per the fork note in the bug doc).
|
|
11
|
+
*
|
|
12
|
+
* Prop-capturing canvas mocks pin the contract: mounted, given the
|
|
13
|
+
* annotations, read-only, session threaded. Started RED (image: no canvas at
|
|
14
|
+
* all; pdf: no session) → GREEN.
|
|
15
|
+
*/
|
|
16
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
17
|
+
import { render, within } from '@testing-library/react';
|
|
18
|
+
import '@testing-library/jest-dom';
|
|
19
|
+
import type { SemiontSession } from '@semiont/sdk';
|
|
20
|
+
import type { Annotation, AnnotationId } from '@semiont/core';
|
|
21
|
+
import { BrowseView } from '../BrowseView';
|
|
22
|
+
|
|
23
|
+
const captured = vi.hoisted(() => ({
|
|
24
|
+
svg: [] as Record<string, unknown>[],
|
|
25
|
+
pdf: [] as Record<string, unknown>[],
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
vi.mock('../../image-annotation/SvgDrawingCanvas', () => ({
|
|
29
|
+
SvgDrawingCanvas: (props: Record<string, unknown>) => {
|
|
30
|
+
captured.svg.push(props);
|
|
31
|
+
return <div className="semiont-svg-drawing-canvas">svg-canvas-mock</div>;
|
|
32
|
+
},
|
|
33
|
+
}));
|
|
34
|
+
vi.mock('../../pdf-annotation/PdfAnnotationCanvas.client', () => ({
|
|
35
|
+
PdfAnnotationCanvas: (props: Record<string, unknown>) => {
|
|
36
|
+
captured.pdf.push(props);
|
|
37
|
+
return <div>pdf-canvas-mock</div>;
|
|
38
|
+
},
|
|
39
|
+
}));
|
|
40
|
+
vi.mock('../../annotation/AnnotateToolbar', () => ({ AnnotateToolbar: () => null }));
|
|
41
|
+
vi.mock('react-markdown', () => ({ default: ({ children }: { children: string }) => <div>{children}</div> }));
|
|
42
|
+
vi.mock('remark-gfm', () => ({ default: () => ({}) }));
|
|
43
|
+
|
|
44
|
+
function fakeSession(): SemiontSession {
|
|
45
|
+
return {
|
|
46
|
+
client: { browse: { click: vi.fn() }, beckon: { hover: vi.fn() } },
|
|
47
|
+
subscribe: () => () => {},
|
|
48
|
+
} as unknown as SemiontSession;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A shape annotation (SvgSelector region) — the image-annotation shape. */
|
|
52
|
+
function shapeAnnotation(): Annotation {
|
|
53
|
+
return {
|
|
54
|
+
'@context': 'http://www.w3.org/ns/anno.jsonld',
|
|
55
|
+
id: 'shape-1' as AnnotationId,
|
|
56
|
+
type: 'Annotation',
|
|
57
|
+
motivation: 'highlighting',
|
|
58
|
+
creator: { '@type': 'Person', name: 'user@example.com' },
|
|
59
|
+
created: '2026-07-14T00:00:00Z',
|
|
60
|
+
target: {
|
|
61
|
+
source: 'res-1',
|
|
62
|
+
selector: { type: 'SvgSelector', value: '<svg><rect x="1" y="1" width="5" height="5"/></svg>' },
|
|
63
|
+
},
|
|
64
|
+
} as unknown as Annotation;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const emptyAnnotations = { highlights: [], references: [], assessments: [], comments: [], tags: [] };
|
|
68
|
+
|
|
69
|
+
const baseProps = {
|
|
70
|
+
resourceUri: 'res-1',
|
|
71
|
+
annotateMode: false,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
describe('browse-renderers — annotation + session forwarding (dispatch contract)', () => {
|
|
75
|
+
beforeEach(() => {
|
|
76
|
+
vi.clearAllMocks();
|
|
77
|
+
captured.svg.length = 0;
|
|
78
|
+
captured.pdf.length = 0;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('image browse mounts the read-only annotation canvas with the annotations and session', () => {
|
|
82
|
+
const session = fakeSession();
|
|
83
|
+
const annotations = { ...emptyAnnotations, highlights: [shapeAnnotation()] };
|
|
84
|
+
const { container } = render(
|
|
85
|
+
<BrowseView {...baseProps} content="blob:image-url" mimeType="image/png"
|
|
86
|
+
annotations={annotations} session={session} />,
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
expect(within(container).getByText('svg-canvas-mock')).toBeInTheDocument();
|
|
90
|
+
expect(captured.svg).toHaveLength(1);
|
|
91
|
+
const props = captured.svg[0]!;
|
|
92
|
+
expect(props.imageUrl).toBe('blob:image-url');
|
|
93
|
+
expect(props.existingAnnotations).toEqual([shapeAnnotation()]); // the dropped prop
|
|
94
|
+
expect(props.drawingMode).toBeNull(); // read-only in browse
|
|
95
|
+
expect(props.session).toBe(session); // interaction parity
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('pdf browse keeps its annotations (pinned) and gains the session (the healed click gap)', async () => {
|
|
99
|
+
const session = fakeSession();
|
|
100
|
+
const annotations = { ...emptyAnnotations, highlights: [shapeAnnotation()] };
|
|
101
|
+
const { container } = render(
|
|
102
|
+
<BrowseView {...baseProps} content="blob:pdf-url" mimeType="application/pdf"
|
|
103
|
+
annotations={annotations} session={session} />,
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
await within(container).findByText('pdf-canvas-mock'); // flush the lazy canvas
|
|
107
|
+
expect(captured.pdf).toHaveLength(1);
|
|
108
|
+
const props = captured.pdf[0]!;
|
|
109
|
+
expect(props.existingAnnotations).toEqual([shapeAnnotation()]); // pinned — worked before
|
|
110
|
+
expect(props.drawingMode).toBeNull();
|
|
111
|
+
expect(props.session).toBe(session); // NEW — was a session-less no-op
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EMBEDDABLE-VIEWER-COMPLETION Phase 0 — regression: the browse `text` default
|
|
3
|
+
* renders markdown as **formatted prose**, not raw source.
|
|
4
|
+
*
|
|
5
|
+
* The other BrowseView suites mock `react-markdown` away for simplicity, so
|
|
6
|
+
* nothing currently pins that the default text renderer actually *formats*
|
|
7
|
+
* markdown. This spec uses the real renderer and guards against a regression to
|
|
8
|
+
* raw/source rendering.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, it, expect } from 'vitest';
|
|
11
|
+
import { render } from '@testing-library/react';
|
|
12
|
+
import '@testing-library/jest-dom';
|
|
13
|
+
import { TextBrowseRenderer, defaultBrowseRenderers } from '../browse-renderers';
|
|
14
|
+
|
|
15
|
+
const base = { mimeType: 'text/markdown', resourceUri: 'res-1', annotations: [] };
|
|
16
|
+
|
|
17
|
+
describe('browse-renderers — markdown-as-prose (Phase 0 regression)', () => {
|
|
18
|
+
it('TextBrowseRenderer is the default `text` renderer', () => {
|
|
19
|
+
expect(defaultBrowseRenderers.text).toBe(TextBrowseRenderer);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('renders markdown as formatted HTML, not raw source', () => {
|
|
23
|
+
const { container } = render(
|
|
24
|
+
<TextBrowseRenderer content={'# Big Heading\n\nsome **bold** and a [link](https://x.test).'} {...base} />,
|
|
25
|
+
);
|
|
26
|
+
// Formatted: heading / strong / anchor elements exist (react-markdown output).
|
|
27
|
+
expect(container.querySelector('h1')?.textContent).toBe('Big Heading');
|
|
28
|
+
expect(container.querySelector('strong')?.textContent).toBe('bold');
|
|
29
|
+
expect(container.querySelector('a')?.getAttribute('href')).toBe('https://x.test');
|
|
30
|
+
// NOT raw: the literal markdown syntax must not survive to the text content.
|
|
31
|
+
expect(container.textContent).not.toContain('# Big Heading');
|
|
32
|
+
expect(container.textContent).not.toContain('**bold**');
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EMBEDDABLE-VIEWER-COMPLETION Phase 1 — inline embedding.
|
|
3
|
+
*
|
|
4
|
+
* `inline` is a display variant (default: today's pane behavior, frontend
|
|
5
|
+
* untouched): the viewer renders at content height in a bare container — no
|
|
6
|
+
* inner scroll container, no pane chrome. jsdom can't compute stylesheet
|
|
7
|
+
* layout, so this spec pins the two testable seams: (1) the components emit
|
|
8
|
+
* the `--inline` modifier classes; (2) the stylesheet contains the inline
|
|
9
|
+
* overrides (height:auto / overflow:visible / padding drop). The visual
|
|
10
|
+
* auto-height check is the plan's live smoke-test.
|
|
11
|
+
*
|
|
12
|
+
* Started RED (no `inline` prop) and GREEN once Phase 1 lands.
|
|
13
|
+
*/
|
|
14
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
15
|
+
import { render } from '@testing-library/react';
|
|
16
|
+
import '@testing-library/jest-dom';
|
|
17
|
+
import { readFileSync } from 'node:fs';
|
|
18
|
+
import { resolve } from 'node:path';
|
|
19
|
+
import type { SemiontSession } from '@semiont/sdk';
|
|
20
|
+
import type { ResourceDescriptor as SemiontResource, ResourceId } from '@semiont/core';
|
|
21
|
+
import { BrowseView } from '../BrowseView';
|
|
22
|
+
import { ResourceViewer } from '../ResourceViewer';
|
|
23
|
+
|
|
24
|
+
vi.mock('../../annotation/AnnotateToolbar', () => ({ AnnotateToolbar: () => null }));
|
|
25
|
+
vi.mock('react-markdown', () => ({ default: ({ children }: { children: string }) => <div>{children}</div> }));
|
|
26
|
+
vi.mock('remark-gfm', () => ({ default: () => ({}) }));
|
|
27
|
+
|
|
28
|
+
const emptyAnnotations = { highlights: [], references: [], assessments: [], comments: [], tags: [] };
|
|
29
|
+
|
|
30
|
+
function fakeSession(): SemiontSession {
|
|
31
|
+
return {
|
|
32
|
+
client: { browse: { click: vi.fn() }, beckon: { hover: vi.fn() } },
|
|
33
|
+
subscribe: () => () => {},
|
|
34
|
+
} as unknown as SemiontSession;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const resource: SemiontResource & { content: string } = {
|
|
38
|
+
'@context': 'https://www.w3.org/ns/activitystreams',
|
|
39
|
+
'@id': 'res-1' as ResourceId,
|
|
40
|
+
name: 'Doc',
|
|
41
|
+
created: '2024-01-01T00:00:00Z',
|
|
42
|
+
entityTypes: [],
|
|
43
|
+
archived: false,
|
|
44
|
+
representations: [{ mediaType: 'text/plain', byteSize: 10 }],
|
|
45
|
+
content: 'Inline content.',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
describe('inline embedding (Phase 1)', () => {
|
|
49
|
+
it('BrowseView: `inline` adds the modifier class; default does not', () => {
|
|
50
|
+
const props = {
|
|
51
|
+
content: 'x', mimeType: 'text/plain', resourceUri: 'res-1',
|
|
52
|
+
annotations: emptyAnnotations, annotateMode: false, session: fakeSession(),
|
|
53
|
+
};
|
|
54
|
+
const { container: pane } = render(<BrowseView {...props} />);
|
|
55
|
+
expect(pane.querySelector('.semiont-browse-view')).not.toHaveClass('semiont-browse-view--inline');
|
|
56
|
+
|
|
57
|
+
const { container } = render(<BrowseView {...props} inline />);
|
|
58
|
+
expect(container.querySelector('.semiont-browse-view')).toHaveClass('semiont-browse-view--inline');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('ResourceViewer: `inline` marks its root and threads to BrowseView', () => {
|
|
62
|
+
const { container } = render(
|
|
63
|
+
<ResourceViewer
|
|
64
|
+
resource={resource} annotations={emptyAnnotations}
|
|
65
|
+
session={fakeSession()} inline
|
|
66
|
+
/>,
|
|
67
|
+
);
|
|
68
|
+
expect(container.querySelector('.semiont-resource-viewer')).toHaveClass('semiont-resource-viewer--inline');
|
|
69
|
+
expect(container.querySelector('.semiont-browse-view')).toHaveClass('semiont-browse-view--inline');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('stylesheet carries the inline overrides (static gate — layout itself is the live check)', () => {
|
|
73
|
+
// vitest runs with cwd = the package root (same in CI's per-workspace run).
|
|
74
|
+
const css = readFileSync(resolve(process.cwd(), 'src/styles/features/resource-viewer.css'), 'utf8');
|
|
75
|
+
// Auto-height roots, and the content area stops being a scroll container / drops the pane gutter.
|
|
76
|
+
expect(css).toMatch(/\.semiont-resource-viewer--inline\s*\{[^}]*height:\s*auto/);
|
|
77
|
+
expect(css).toMatch(/\.semiont-browse-view--inline\s*\{[^}]*height:\s*auto/);
|
|
78
|
+
expect(css).toMatch(/\.semiont-browse-view--inline\s+\.semiont-browse-view__content\s*\{[^}]*overflow:\s*visible/);
|
|
79
|
+
expect(css).toMatch(/\.semiont-browse-view--inline\s+\.semiont-browse-view__content\s*\{[^}]*padding:\s*0/);
|
|
80
|
+
});
|
|
81
|
+
});
|