@bendyline/squisq-editor-react 1.6.2 → 2.0.0
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 +12 -3
- package/dist/index.d.ts +1229 -179
- package/dist/index.js +18017 -11231
- package/dist/index.js.map +1 -1
- package/dist/monaco.d.ts +1 -0
- package/dist/monaco.js +4 -0
- package/dist/monaco.js.map +1 -0
- package/dist/styles/index.css +1471 -245
- package/package.json +9 -5
- package/src/BlockPropertiesPopover.tsx +30 -15
- package/src/DocumentSettingsDialog.tsx +13 -21
- package/src/EditorContext.tsx +199 -42
- package/src/EditorShell.tsx +348 -260
- package/src/ImageEditor.tsx +22 -1
- package/src/InlinePreviewGutter.tsx +16 -8
- package/src/MediaBin.tsx +107 -14
- package/src/OutlinePanel.tsx +202 -7
- package/src/PlainHtmlPreview.tsx +40 -1
- package/src/PreviewControls.tsx +309 -69
- package/src/PreviewPanel.tsx +312 -78
- package/src/RawEditor.tsx +13 -2
- package/src/StatusBar.tsx +5 -2
- package/src/TemplateAnnotation.ts +12 -3
- package/src/TemplatePicker.tsx +75 -21
- package/src/ThemeCustomizerPanel.tsx +8 -0
- package/src/ThemePicker.tsx +10 -5
- package/src/TimelineBlockPreview.tsx +1 -1
- package/src/Toolbar.tsx +202 -41
- package/src/TransitionPicker.tsx +36 -4
- package/src/ViewMenuPanel.tsx +12 -14
- package/src/WysiwygEditor.tsx +124 -91
- package/src/__tests__/blockPropertiesPopoverTheme.test.tsx +33 -0
- package/src/__tests__/blockTagActivity.test.ts +183 -0
- package/src/__tests__/buildDocumentPreviewMarkdown.test.ts +75 -0
- package/src/__tests__/buildPreviewDocContent.test.ts +48 -0
- package/src/__tests__/buildPreviewDocTransition.test.ts +21 -1
- package/src/__tests__/canvasSurfaceTextEditing.test.tsx +60 -0
- package/src/__tests__/customLayoutManagerTheme.test.tsx +34 -0
- package/src/__tests__/documentSettingsDialog.test.tsx +29 -1
- package/src/__tests__/editorScrollHandoff.test.tsx +102 -0
- package/src/__tests__/editorScrollSync.test.ts +65 -0
- package/src/__tests__/editorShellProps.test.tsx +123 -1
- package/src/__tests__/emojiPicker.test.tsx +2 -42
- package/src/__tests__/headingTransition.test.ts +0 -12
- package/src/__tests__/imageEditAffordance.test.tsx +5 -1
- package/src/__tests__/imageEditModalContract.test.tsx +119 -0
- package/src/__tests__/imageEditorShell.test.tsx +15 -1
- package/src/__tests__/jsonEditor.test.tsx +56 -1
- package/src/__tests__/layersPanel.test.tsx +75 -5
- package/src/__tests__/majorApiTypes.test.ts +26 -0
- package/src/__tests__/mediaBinDrop.test.tsx +90 -0
- package/src/__tests__/narrationSave.test.ts +148 -0
- package/src/__tests__/outlinePanel.test.tsx +93 -3
- package/src/__tests__/outlineSource.test.ts +217 -0
- package/src/__tests__/plainHtmlPreview.test.tsx +17 -1
- package/src/__tests__/presentationMode.test.tsx +290 -0
- package/src/__tests__/previewControls.test.tsx +218 -5
- package/src/__tests__/previewPanelAudioFailure.test.tsx +46 -0
- package/src/__tests__/previewPanelPresentation.test.tsx +126 -0
- package/src/__tests__/previewPanelTransforms.test.tsx +117 -0
- package/src/__tests__/rawEditorModelIsolation.test.ts +19 -0
- package/src/__tests__/resolveBlockVisual.test.ts +43 -0
- package/src/__tests__/statusBar.test.tsx +27 -0
- package/src/__tests__/teleprompter.test.tsx +190 -0
- package/src/__tests__/templateAnnotationRoundTrip.test.ts +26 -0
- package/src/__tests__/templatePickerPortal.test.tsx +62 -0
- package/src/__tests__/tiptapBridge.test.ts +23 -0
- package/src/__tests__/tiptapCodeBlockRoundTrip.test.ts +109 -0
- package/src/__tests__/transformStyleId.test.ts +13 -0
- package/src/__tests__/useImageEditor.test.tsx +67 -0
- package/src/__tests__/useMediaRecorder.test.ts +75 -0
- package/src/__tests__/viewMenuPanel.test.tsx +130 -0
- package/src/__tests__/wysiwygImageUpload.test.ts +69 -0
- package/src/__tests__/wysiwygTemplateBadgeFocus.test.tsx +56 -0
- package/src/asciiDiagram/AsciiDiagramExtension.ts +343 -0
- package/src/asciiDiagram/AsciiDiagramWidget.tsx +224 -0
- package/src/asciiDiagram/RepairableDiagramExtension.ts +200 -0
- package/src/asciiDiagram/__tests__/AsciiDiagramExtension.test.ts +145 -0
- package/src/asciiDiagram/__tests__/RepairableDiagramExtension.test.ts +159 -0
- package/src/asciiDiagram/__tests__/asciiDiagramCommands.test.ts +227 -0
- package/src/asciiDiagram/__tests__/asciiDiagramOps.test.ts +114 -0
- package/src/asciiDiagram/__tests__/asciiPaste.test.ts +43 -0
- package/src/asciiDiagram/asciiDiagramCommands.ts +148 -0
- package/src/asciiDiagram/asciiDiagramData.ts +116 -0
- package/src/asciiDiagram/asciiDiagramOps.ts +219 -0
- package/src/asciiDiagram/asciiPaste.ts +19 -0
- package/src/blockTagActivity.ts +233 -0
- package/src/buildDocumentPreviewMarkdown.ts +168 -0
- package/src/buildPreviewDoc.ts +19 -12
- package/src/customTemplates/CustomLayoutManager.tsx +3 -2
- package/src/customTemplates/CustomTemplateContext.tsx +6 -0
- package/src/customTemplates/TemplateDesigner.tsx +7 -4
- package/src/customTemplates/__tests__/library.test.ts +8 -0
- package/src/customTemplates/designer.css +158 -99
- package/src/customTemplates/library.ts +16 -3
- package/src/customThemes/CustomThemeContext.tsx +6 -0
- package/src/customThemes/CustomThemeDialog.tsx +35 -5
- package/src/customThemes/ImportThemeSection.tsx +178 -0
- package/src/customThemes/__tests__/customThemeLibrary.test.ts +9 -0
- package/src/customThemes/__tests__/importThemeSection.test.tsx +192 -0
- package/src/customThemes/customThemeLibrary.ts +7 -4
- package/src/customThemes/index.ts +7 -1
- package/src/customThemes/themeDraft.ts +23 -7
- package/src/diagram/DiagramCanvas.tsx +9 -4
- package/src/diagram/types.ts +35 -0
- package/src/editorScrollSync.ts +73 -0
- package/src/emojiData.ts +3 -23
- package/src/headingTransition.ts +0 -15
- package/src/imageEditor/CanvasSurface.tsx +29 -23
- package/src/imageEditor/LayersPanel.tsx +78 -3
- package/src/imageEditor/Toolbar.tsx +5 -1
- package/src/imageEditor/icons.tsx +4 -0
- package/src/imageEditor/image-editor.css +75 -0
- package/src/imageEditor/layers/SelectionHandles.tsx +1 -1
- package/src/imageEditor/useImageEditor.ts +71 -14
- package/src/index.ts +191 -19
- package/src/jsonEditor/JsonEditorContext.tsx +12 -6
- package/src/jsonEditor/RenderNode.tsx +24 -3
- package/src/jsonEditor/editors.tsx +86 -19
- package/src/mediaEntries.ts +12 -0
- package/src/monaco.ts +35 -0
- package/src/monacoWorkers.ts +89 -0
- package/src/outlineSource.ts +171 -0
- package/src/presentation/PresentationMode.tsx +596 -0
- package/src/rawEditorIsolation.ts +18 -0
- package/src/recorder/hooks/useMediaRecorder.ts +97 -53
- package/src/resolveBlockVisual.ts +10 -4
- package/src/scene/Scene.tsx +48 -18
- package/src/scene/SceneBlockExtension.ts +17 -10
- package/src/scene/SceneBlockWidget.tsx +5 -1
- package/src/scene/SceneViewport.tsx +7 -3
- package/src/scene/ShapePalette.tsx +17 -2
- package/src/scene/__tests__/DiagramAdapter.test.ts +86 -0
- package/src/scene/__tests__/SceneBlockExtension.test.ts +33 -0
- package/src/scene/__tests__/sceneIsolation.test.tsx +93 -0
- package/src/scene/adapters/DiagramAdapter.ts +12 -101
- package/src/scene/commands/SceneCommand.ts +4 -1
- package/src/scene/index.ts +1 -6
- package/src/scene/layers/DiagramEdges.tsx +21 -7
- package/src/scene/layers/edgeGeometry.ts +9 -1
- package/src/scene/layers/nodeCard.tsx +27 -8
- package/src/scene/scene.css +5 -2
- package/src/scene/text/SceneTextOverlay.tsx +5 -5
- package/src/scene/text/sceneTextChannel.ts +26 -20
- package/src/scene/text/sceneTextConfig.ts +4 -0
- package/src/scene/tools/ConnectTool.ts +13 -4
- package/src/scene/tools/SceneTool.ts +8 -0
- package/src/scene/tools/SelectTool.ts +34 -18
- package/src/scene/tools/ShapeTool.ts +2 -3
- package/src/styles/ascii-diagram.css +79 -0
- package/src/styles/ascii-timeline.css +499 -0
- package/src/styles/editor.css +706 -120
- package/src/styles/index.css +3 -0
- package/src/styles/tree-view.css +139 -0
- package/src/teleprompter/TeleprompterControls.tsx +218 -0
- package/src/teleprompter/TeleprompterSelfView.tsx +22 -0
- package/src/teleprompter/TeleprompterSurface.tsx +267 -0
- package/src/teleprompter/TeleprompterView.tsx +338 -0
- package/src/teleprompter/canvasRenderer.ts +161 -0
- package/src/teleprompter/floatingWindow.ts +352 -0
- package/src/teleprompter/index.ts +61 -0
- package/src/teleprompter/pcmWorklet.ts +62 -0
- package/src/teleprompter/recording/insertPreamble.ts +80 -0
- package/src/teleprompter/recording/narrationSave.ts +134 -0
- package/src/teleprompter/recording/useNarrationRecorder.ts +367 -0
- package/src/teleprompter/scrollModel.ts +85 -0
- package/src/teleprompter/teleprompterTheme.ts +303 -0
- package/src/teleprompter/types.ts +38 -0
- package/src/teleprompter/useFloatingWindow.ts +74 -0
- package/src/teleprompter/useMicAnalysis.ts +211 -0
- package/src/teleprompter/useTeleprompter.ts +421 -0
- package/src/templateContentPreviewResolver.ts +12 -5
- package/src/timeline/TimelineEditorWidget.tsx +702 -0
- package/src/timeline/TimelineViewExtension.ts +252 -0
- package/src/timeline/__tests__/TimelineEditorWidget.test.tsx +355 -0
- package/src/timeline/__tests__/TimelineViewExtension.test.ts +163 -0
- package/src/timeline/__tests__/timelineCommands.test.ts +327 -0
- package/src/timeline/__tests__/timelineOps.test.ts +222 -0
- package/src/timeline/__tests__/timelinePaste.test.ts +34 -0
- package/src/timeline/timelineCommands.ts +367 -0
- package/src/timeline/timelineData.ts +54 -0
- package/src/timeline/timelineOps.ts +277 -0
- package/src/timeline/timelinePaste.ts +8 -0
- package/src/tiptapBridge.ts +24 -16
- package/src/transformStyleId.ts +17 -0
- package/src/treeview/TreeOutlineWidget.tsx +240 -0
- package/src/treeview/TreeViewExtension.ts +250 -0
- package/src/treeview/__tests__/TreeViewExtension.test.ts +122 -0
- package/src/treeview/__tests__/treeOps.test.ts +125 -0
- package/src/treeview/__tests__/treePaste.test.ts +40 -0
- package/src/treeview/__tests__/treeViewCommands.test.ts +163 -0
- package/src/treeview/treeOps.ts +172 -0
- package/src/treeview/treePaste.ts +13 -0
- package/src/treeview/treeViewCommands.ts +94 -0
- package/src/treeview/treeViewData.ts +41 -0
- package/src/useMonacoLoader.ts +11 -34
- package/src/wysiwygImageUpload.ts +113 -0
- package/src/diagram/DiagramExtension.ts +0 -209
- package/src/diagram/DiagramWidget.tsx +0 -272
- package/src/diagram/useDiagramData.ts +0 -126
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* @vitest-environment jsdom
|
|
3
3
|
*/
|
|
4
4
|
import { afterEach, describe, expect, it } from 'vitest';
|
|
5
|
-
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
5
|
+
import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
|
|
6
|
+
import { compileTheme } from '@bendyline/squisq/schemas';
|
|
6
7
|
import { EditorProvider, useEditorContext } from '../EditorContext';
|
|
7
8
|
import {
|
|
8
9
|
PreviewModeSwitch,
|
|
@@ -10,10 +11,25 @@ import {
|
|
|
10
11
|
PreviewToolbarControls,
|
|
11
12
|
usePreviewSettings,
|
|
12
13
|
} from '../PreviewControls';
|
|
14
|
+
import { Toolbar } from '../Toolbar';
|
|
15
|
+
import {
|
|
16
|
+
clearThemeLibrary,
|
|
17
|
+
CustomThemeProvider,
|
|
18
|
+
saveLibraryTheme,
|
|
19
|
+
useDocCustomThemes,
|
|
20
|
+
} from '../customThemes';
|
|
13
21
|
|
|
14
22
|
function ModeProbe() {
|
|
15
|
-
const { activeDisplayMode } = usePreviewSettings();
|
|
16
|
-
return
|
|
23
|
+
const { activeDisplayMode, activeThemeId, activeTransformStyle } = usePreviewSettings();
|
|
24
|
+
return (
|
|
25
|
+
<div
|
|
26
|
+
data-testid="active-mode"
|
|
27
|
+
data-theme-id={activeThemeId}
|
|
28
|
+
data-transform-style={activeTransformStyle}
|
|
29
|
+
>
|
|
30
|
+
{activeDisplayMode}
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
17
33
|
}
|
|
18
34
|
|
|
19
35
|
function PreviewHarness() {
|
|
@@ -35,6 +51,43 @@ function PreviewToolbarHarness() {
|
|
|
35
51
|
);
|
|
36
52
|
}
|
|
37
53
|
|
|
54
|
+
function UseTabHarness() {
|
|
55
|
+
const { activeView, doc } = useEditorContext();
|
|
56
|
+
return (
|
|
57
|
+
<PreviewSettingsProvider doc={doc}>
|
|
58
|
+
<Toolbar />
|
|
59
|
+
<ModeProbe />
|
|
60
|
+
<div data-testid="active-view">{activeView}</div>
|
|
61
|
+
</PreviewSettingsProvider>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function LibraryThemeProbe() {
|
|
66
|
+
const { activeTheme, setSelectedThemeId } = usePreviewSettings();
|
|
67
|
+
const { markdownSource } = useEditorContext();
|
|
68
|
+
return (
|
|
69
|
+
<>
|
|
70
|
+
<button type="button" onClick={() => setSelectedThemeId('library-theme')}>
|
|
71
|
+
Select library theme
|
|
72
|
+
</button>
|
|
73
|
+
<div data-testid="active-theme-name">{activeTheme.name}</div>
|
|
74
|
+
<pre data-testid="markdown-source">{markdownSource}</pre>
|
|
75
|
+
</>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function LibraryThemeHarness() {
|
|
80
|
+
const { doc } = useEditorContext();
|
|
81
|
+
const { docThemes, onDocThemesChange } = useDocCustomThemes();
|
|
82
|
+
return (
|
|
83
|
+
<CustomThemeProvider docThemes={docThemes} onDocThemesChange={onDocThemesChange}>
|
|
84
|
+
<PreviewSettingsProvider doc={doc}>
|
|
85
|
+
<LibraryThemeProbe />
|
|
86
|
+
</PreviewSettingsProvider>
|
|
87
|
+
</CustomThemeProvider>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
38
91
|
function renderPreviewControls(markdown: string) {
|
|
39
92
|
render(
|
|
40
93
|
<EditorProvider initialMarkdown={markdown}>
|
|
@@ -51,7 +104,10 @@ function renderPreviewToolbar(markdown: string) {
|
|
|
51
104
|
);
|
|
52
105
|
}
|
|
53
106
|
|
|
54
|
-
afterEach(() =>
|
|
107
|
+
afterEach(() => {
|
|
108
|
+
cleanup();
|
|
109
|
+
clearThemeLibrary();
|
|
110
|
+
});
|
|
55
111
|
|
|
56
112
|
describe('PreviewModeSwitch', () => {
|
|
57
113
|
it('labels the plain document preview as Document and the styled view as Page', () => {
|
|
@@ -62,7 +118,8 @@ describe('PreviewModeSwitch', () => {
|
|
|
62
118
|
.map((button) => button.textContent)
|
|
63
119
|
.filter(Boolean);
|
|
64
120
|
|
|
65
|
-
expect(labels).toEqual(['
|
|
121
|
+
expect(labels).toEqual(['Slideshow', 'Video', 'Page', 'Document', 'Narrate']);
|
|
122
|
+
expect(screen.getByTestId('active-mode').textContent).toBe('slideshow');
|
|
66
123
|
|
|
67
124
|
fireEvent.click(screen.getByRole('button', { name: 'Document' }));
|
|
68
125
|
expect(screen.getByTestId('active-mode').textContent).toBe('page');
|
|
@@ -89,9 +146,83 @@ describe('PreviewModeSwitch', () => {
|
|
|
89
146
|
});
|
|
90
147
|
expect(screen.getByRole('button', { name: 'Page' }).getAttribute('aria-pressed')).toBe('true');
|
|
91
148
|
});
|
|
149
|
+
|
|
150
|
+
it('uses the shared squisq-theme, themeId, theme fallback order', async () => {
|
|
151
|
+
renderPreviewControls('---\nthemeId: bold\ntheme: cinematic\n---\n\n# Hello');
|
|
152
|
+
|
|
153
|
+
await waitFor(() => {
|
|
154
|
+
expect(screen.getByTestId('active-mode').getAttribute('data-theme-id')).toBe('bold');
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('canonicalizes the stored dataDriven transform id', async () => {
|
|
159
|
+
renderPreviewControls('---\nsquisq-transform: dataDriven\n---\n\n# Hello');
|
|
160
|
+
|
|
161
|
+
await waitFor(() => {
|
|
162
|
+
expect(screen.getByTestId('active-mode').getAttribute('data-transform-style')).toBe(
|
|
163
|
+
'data-driven',
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('copies a selected browser-library theme into the document atomically', async () => {
|
|
169
|
+
saveLibraryTheme(
|
|
170
|
+
compileTheme({
|
|
171
|
+
id: 'library-theme',
|
|
172
|
+
name: 'Library Theme',
|
|
173
|
+
seedColors: { primary: '#456789' },
|
|
174
|
+
}),
|
|
175
|
+
);
|
|
176
|
+
render(
|
|
177
|
+
<EditorProvider initialMarkdown="# Hello">
|
|
178
|
+
<LibraryThemeHarness />
|
|
179
|
+
</EditorProvider>,
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
fireEvent.click(screen.getByRole('button', { name: 'Select library theme' }));
|
|
183
|
+
|
|
184
|
+
await waitFor(() => {
|
|
185
|
+
expect(screen.getByTestId('active-theme-name').textContent).toBe('Library Theme');
|
|
186
|
+
const source = screen.getByTestId('markdown-source').textContent ?? '';
|
|
187
|
+
expect(source).toContain('squisq-theme: library-theme');
|
|
188
|
+
expect(source).toContain('squisq-custom-themes:');
|
|
189
|
+
expect(source).toContain('library-theme');
|
|
190
|
+
});
|
|
191
|
+
});
|
|
92
192
|
});
|
|
93
193
|
|
|
94
194
|
describe('PreviewToolbarControls', () => {
|
|
195
|
+
it('presents transforms as summarization without implying the source is changed', () => {
|
|
196
|
+
const originalResizeObserver = globalThis.ResizeObserver;
|
|
197
|
+
class ResizeObserverStub implements ResizeObserver {
|
|
198
|
+
observe() {}
|
|
199
|
+
unobserve() {}
|
|
200
|
+
disconnect() {}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
globalThis.ResizeObserver = ResizeObserverStub;
|
|
204
|
+
try {
|
|
205
|
+
renderPreviewToolbar('# Hello');
|
|
206
|
+
|
|
207
|
+
const summarizeLabels = screen.getAllByText('Summarize:');
|
|
208
|
+
expect(summarizeLabels.length).toBeGreaterThan(0);
|
|
209
|
+
for (const label of summarizeLabels) {
|
|
210
|
+
expect(label.getAttribute('title')).toBe(
|
|
211
|
+
'Extract and summarize content for presentation with these Use modes. Your underlying content is not changed.',
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
expect(screen.queryByText('Transform:')).toBeNull();
|
|
215
|
+
expect(document.querySelector('[role="group"][aria-label="Display mode"]')).toBeNull();
|
|
216
|
+
expect(document.querySelector('[role="group"][aria-label="Aspect ratio"]')).not.toBeNull();
|
|
217
|
+
} finally {
|
|
218
|
+
if (originalResizeObserver) {
|
|
219
|
+
globalThis.ResizeObserver = originalResizeObserver;
|
|
220
|
+
} else {
|
|
221
|
+
Reflect.deleteProperty(globalThis, 'ResizeObserver');
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
95
226
|
it('keeps the overflow popover inside the left viewport edge', async () => {
|
|
96
227
|
const originalResizeObserver = globalThis.ResizeObserver;
|
|
97
228
|
const originalGetBoundingClientRect = HTMLElement.prototype.getBoundingClientRect;
|
|
@@ -161,3 +292,85 @@ describe('PreviewToolbarControls', () => {
|
|
|
161
292
|
}
|
|
162
293
|
});
|
|
163
294
|
});
|
|
295
|
+
|
|
296
|
+
describe('Use tab mode menu', () => {
|
|
297
|
+
it('lists the Use modes beside the tab and selects a mode without a toolbar radio group', () => {
|
|
298
|
+
render(
|
|
299
|
+
<EditorProvider initialMarkdown="# Hello" initialView="wysiwyg" allowRecording={false}>
|
|
300
|
+
<UseTabHarness />
|
|
301
|
+
</EditorProvider>,
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
const useTab = screen.getByRole('tab', { name: 'Slideshow' });
|
|
305
|
+
expect(screen.queryByRole('tab', { name: 'Use' })).toBeNull();
|
|
306
|
+
const tabGroup = useTab.parentElement;
|
|
307
|
+
expect(tabGroup).not.toBeNull();
|
|
308
|
+
expect(within(tabGroup!).getByRole('button', { name: 'Choose Use mode' })).toBeTruthy();
|
|
309
|
+
|
|
310
|
+
// The first click enters Use; once active, clicking the tab again opens
|
|
311
|
+
// the mode menu.
|
|
312
|
+
fireEvent.click(useTab);
|
|
313
|
+
expect(screen.getByTestId('active-view').textContent).toBe('preview');
|
|
314
|
+
expect(screen.queryByRole('menu', { name: 'Use mode' })).toBeNull();
|
|
315
|
+
|
|
316
|
+
fireEvent.click(screen.getByRole('tab', { name: 'Slideshow' }));
|
|
317
|
+
|
|
318
|
+
const menu = screen.getByRole('menu', { name: 'Use mode' });
|
|
319
|
+
expect(
|
|
320
|
+
within(menu)
|
|
321
|
+
.getAllByRole('menuitemradio')
|
|
322
|
+
.map((item) => item.querySelector('.squisq-use-mode-menu-label')?.textContent),
|
|
323
|
+
).toEqual(['Slideshow', 'Video', 'Page', 'Document', 'Narrate']);
|
|
324
|
+
const slideshowItem = within(menu).getByRole('menuitemradio', { name: 'Slideshow' });
|
|
325
|
+
expect(slideshowItem.getAttribute('aria-checked')).toBe('true');
|
|
326
|
+
expect(within(slideshowItem).getByText('Present designed slides one at a time.')).toBeTruthy();
|
|
327
|
+
|
|
328
|
+
fireEvent.click(within(menu).getByRole('menuitemradio', { name: 'Document' }));
|
|
329
|
+
expect(screen.getByTestId('active-mode').textContent).toBe('page');
|
|
330
|
+
expect(screen.getByTestId('active-view').textContent).toBe('preview');
|
|
331
|
+
expect(screen.getByRole('tab', { name: 'Document' })).toBeTruthy();
|
|
332
|
+
expect(screen.queryByRole('menu', { name: 'Use mode' })).toBeNull();
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
describe('Narrate mode gating', () => {
|
|
337
|
+
function renderWithNarrateGate(markdown: string, allowNarrate: boolean) {
|
|
338
|
+
render(
|
|
339
|
+
<EditorProvider initialMarkdown={markdown} allowNarrate={allowNarrate}>
|
|
340
|
+
<PreviewHarness />
|
|
341
|
+
</EditorProvider>,
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
it('selects narrate from the switch and resolves teleprompter frontmatter aliases', async () => {
|
|
346
|
+
renderPreviewControls('# Hello');
|
|
347
|
+
fireEvent.click(screen.getByRole('button', { name: 'Narrate' }));
|
|
348
|
+
expect(screen.getByTestId('active-mode').textContent).toBe('narrate');
|
|
349
|
+
|
|
350
|
+
cleanup();
|
|
351
|
+
renderPreviewControls('---\ndisplay-mode: teleprompter\n---\n\n# Hello');
|
|
352
|
+
await waitFor(() => {
|
|
353
|
+
expect(screen.getByTestId('active-mode').textContent).toBe('narrate');
|
|
354
|
+
});
|
|
355
|
+
expect(screen.getByRole('button', { name: 'Narrate' }).getAttribute('aria-pressed')).toBe(
|
|
356
|
+
'true',
|
|
357
|
+
);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('allowNarrate=false hides the button and clamps frontmatter-forced narrate to video', async () => {
|
|
361
|
+
renderWithNarrateGate('---\ndisplay-mode: narrate\n---\n\n# Hello', false);
|
|
362
|
+
|
|
363
|
+
expect(screen.queryByRole('button', { name: 'Narrate' })).toBeNull();
|
|
364
|
+
// Give the frontmatter parse a tick, then confirm the clamp held.
|
|
365
|
+
await waitFor(() => {
|
|
366
|
+
expect(screen.getByTestId('active-mode').textContent).toBe('video');
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('allowNarrate=true (default) honors frontmatter narrate', async () => {
|
|
371
|
+
renderWithNarrateGate('---\ndisplay-mode: narrate\n---\n\n# Hello', true);
|
|
372
|
+
await waitFor(() => {
|
|
373
|
+
expect(screen.getByTestId('active-mode').textContent).toBe('narrate');
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** @vitest-environment jsdom */
|
|
2
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { MemoryContentContainer } from '@bendyline/squisq/storage';
|
|
5
|
+
|
|
6
|
+
const resolveAudioMapping = vi.hoisted(() =>
|
|
7
|
+
vi.fn(async () => {
|
|
8
|
+
throw new Error('timing.json is corrupt');
|
|
9
|
+
}),
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
vi.mock('@bendyline/squisq/doc', async (importOriginal) => ({
|
|
13
|
+
...(await importOriginal<typeof import('@bendyline/squisq/doc')>()),
|
|
14
|
+
resolveAudioMapping,
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
import { EditorProvider, useEditorContext } from '../EditorContext';
|
|
18
|
+
import { PreviewSettingsProvider } from '../PreviewControls';
|
|
19
|
+
import { PreviewPanel } from '../PreviewPanel';
|
|
20
|
+
|
|
21
|
+
function Harness({ container }: { container: MemoryContentContainer }) {
|
|
22
|
+
const { doc } = useEditorContext();
|
|
23
|
+
return (
|
|
24
|
+
<PreviewSettingsProvider doc={doc}>
|
|
25
|
+
<PreviewPanel workspaceContainer={container} />
|
|
26
|
+
</PreviewSettingsProvider>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('PreviewPanel optional audio mapping', () => {
|
|
31
|
+
it('keeps the synchronous preview when audio discovery rejects', async () => {
|
|
32
|
+
const load = vi.spyOn(HTMLMediaElement.prototype, 'load').mockImplementation(() => undefined);
|
|
33
|
+
const container = new MemoryContentContainer();
|
|
34
|
+
const view = render(
|
|
35
|
+
<EditorProvider initialMarkdown="# Preview\n\nBody" initialView="preview">
|
|
36
|
+
<Harness container={container} />
|
|
37
|
+
</EditorProvider>,
|
|
38
|
+
);
|
|
39
|
+
await waitFor(() => expect(resolveAudioMapping).toHaveBeenCalled());
|
|
40
|
+
await waitFor(() => {
|
|
41
|
+
expect(screen.getByTestId('preview-panel').textContent).not.toContain('No content');
|
|
42
|
+
});
|
|
43
|
+
view.unmount();
|
|
44
|
+
load.mockRestore();
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/** @vitest-environment jsdom */
|
|
2
|
+
|
|
3
|
+
import { useRef } from 'react';
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
6
|
+
import type { Doc } from '@bendyline/squisq/schemas';
|
|
7
|
+
import type { DocPlayerProps, PlaybackState } from '@bendyline/squisq-react';
|
|
8
|
+
|
|
9
|
+
vi.mock('@bendyline/squisq-react', async (importOriginal) => {
|
|
10
|
+
const actual = await importOriginal<typeof import('@bendyline/squisq-react')>();
|
|
11
|
+
const { useEffect } = await import('react');
|
|
12
|
+
const state: PlaybackState = {
|
|
13
|
+
isPlaying: false,
|
|
14
|
+
currentTime: 0,
|
|
15
|
+
totalDuration: 10,
|
|
16
|
+
isCoverVisible: true,
|
|
17
|
+
currentBlockIndex: 0,
|
|
18
|
+
totalBlocks: 2,
|
|
19
|
+
docProgress: 0,
|
|
20
|
+
hasCaptions: false,
|
|
21
|
+
captionsEnabled: false,
|
|
22
|
+
captionMode: 'off',
|
|
23
|
+
currentSegmentIndex: 0,
|
|
24
|
+
currentSegmentName: null,
|
|
25
|
+
currentBlock: null,
|
|
26
|
+
};
|
|
27
|
+
function MockDocPlayer(props: DocPlayerProps & { doc: Doc }) {
|
|
28
|
+
const { onPlaybackStateChange, onControlsReady } = props;
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (props.audioController) return;
|
|
31
|
+
onPlaybackStateChange?.(state);
|
|
32
|
+
onControlsReady?.({
|
|
33
|
+
play: () => undefined,
|
|
34
|
+
pause: () => undefined,
|
|
35
|
+
toggle: () => undefined,
|
|
36
|
+
restart: () => undefined,
|
|
37
|
+
seekTo: () => undefined,
|
|
38
|
+
setCaptionsEnabled: () => undefined,
|
|
39
|
+
cycleCaptionMode: () => undefined,
|
|
40
|
+
});
|
|
41
|
+
}, [onControlsReady, onPlaybackStateChange, props.audioController]);
|
|
42
|
+
return (
|
|
43
|
+
<div
|
|
44
|
+
data-testid="mock-doc-player"
|
|
45
|
+
data-article={props.doc.articleId}
|
|
46
|
+
data-audience={props.audioController ? 'true' : 'false'}
|
|
47
|
+
data-controls={props.showControls ? 'true' : 'false'}
|
|
48
|
+
data-cover={String(props.coverVisible)}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
...actual,
|
|
54
|
+
useMediaProvider: () => null,
|
|
55
|
+
DocPlayer: MockDocPlayer,
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
import { EditorProvider, useEditorContext } from '../EditorContext';
|
|
60
|
+
import { PreviewSettingsProvider } from '../PreviewControls';
|
|
61
|
+
import { PreviewPanel } from '../PreviewPanel';
|
|
62
|
+
import {
|
|
63
|
+
PresentationModeControl,
|
|
64
|
+
PresentationModeProvider,
|
|
65
|
+
} from '../presentation/PresentationMode';
|
|
66
|
+
|
|
67
|
+
function Harness() {
|
|
68
|
+
const { doc } = useEditorContext();
|
|
69
|
+
const rootRef = useRef<HTMLDivElement>(null);
|
|
70
|
+
return (
|
|
71
|
+
<div ref={rootRef}>
|
|
72
|
+
<PreviewSettingsProvider doc={doc}>
|
|
73
|
+
<PresentationModeProvider rootRef={rootRef}>
|
|
74
|
+
<PresentationModeControl />
|
|
75
|
+
<PreviewPanel />
|
|
76
|
+
</PresentationModeProvider>
|
|
77
|
+
</PreviewSettingsProvider>
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
afterEach(() => {
|
|
83
|
+
cleanup();
|
|
84
|
+
vi.restoreAllMocks();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('PreviewPanel presentation audience', () => {
|
|
88
|
+
it('renders a clockless, controlled audience follower in the popup', async () => {
|
|
89
|
+
const popupDocument = document.implementation.createHTMLDocument('');
|
|
90
|
+
const popupEvents = new EventTarget();
|
|
91
|
+
let closed = false;
|
|
92
|
+
const popup = {
|
|
93
|
+
document: popupDocument,
|
|
94
|
+
get closed() {
|
|
95
|
+
return closed;
|
|
96
|
+
},
|
|
97
|
+
close: vi.fn(() => {
|
|
98
|
+
closed = true;
|
|
99
|
+
}),
|
|
100
|
+
focus: vi.fn(),
|
|
101
|
+
addEventListener: popupEvents.addEventListener.bind(popupEvents),
|
|
102
|
+
removeEventListener: popupEvents.removeEventListener.bind(popupEvents),
|
|
103
|
+
} as unknown as Window;
|
|
104
|
+
vi.spyOn(window, 'open').mockReturnValue(popup);
|
|
105
|
+
|
|
106
|
+
render(
|
|
107
|
+
<EditorProvider initialMarkdown="# Audience mirror\n\nBody" initialView="preview">
|
|
108
|
+
<Harness />
|
|
109
|
+
</EditorProvider>,
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
await waitFor(() => expect(screen.getByTestId('mock-doc-player')).toBeTruthy());
|
|
113
|
+
fireEvent.click(screen.getByRole('button', { name: 'Presentation options' }));
|
|
114
|
+
fireEvent.click(screen.getByRole('menuitemradio', { name: /New window/ }));
|
|
115
|
+
fireEvent.click(screen.getByRole('button', { name: 'Present: New window' }));
|
|
116
|
+
|
|
117
|
+
await waitFor(() => {
|
|
118
|
+
const audience = popupDocument.querySelector<HTMLElement>(
|
|
119
|
+
'[data-testid="mock-doc-player"][data-audience="true"]',
|
|
120
|
+
);
|
|
121
|
+
expect(audience).toBeTruthy();
|
|
122
|
+
expect(audience?.dataset.controls).toBe('false');
|
|
123
|
+
expect(audience?.dataset.cover).toBe('true');
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/** @vitest-environment jsdom */
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
4
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import type { Doc } from '@bendyline/squisq/schemas';
|
|
6
|
+
|
|
7
|
+
vi.mock('@bendyline/squisq-react', async (importOriginal) => {
|
|
8
|
+
const actual = await importOriginal<typeof import('@bendyline/squisq-react')>();
|
|
9
|
+
const blockSummary = (doc: Doc) =>
|
|
10
|
+
JSON.stringify(doc.blocks.map((block) => ({ id: block.id, template: block.template })));
|
|
11
|
+
return {
|
|
12
|
+
...actual,
|
|
13
|
+
useMediaProvider: () => null,
|
|
14
|
+
DocPlayer: ({ doc }: { doc: Doc }) => (
|
|
15
|
+
<div data-testid="mock-doc-player">{blockSummary(doc)}</div>
|
|
16
|
+
),
|
|
17
|
+
LinearDocView: ({ doc }: { doc: Doc }) => (
|
|
18
|
+
<div data-testid="mock-linear-doc">{blockSummary(doc)}</div>
|
|
19
|
+
),
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
vi.mock('../PlainHtmlPreview', () => ({
|
|
24
|
+
PlainHtmlPreview: ({ markdown }: { markdown: string }) => (
|
|
25
|
+
<div data-testid="mock-plain-document">{markdown}</div>
|
|
26
|
+
),
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
import { EditorProvider, useEditorContext } from '../EditorContext';
|
|
30
|
+
import { PreviewSettingsProvider, usePreviewSettings } from '../PreviewControls';
|
|
31
|
+
import { PreviewPanel } from '../PreviewPanel';
|
|
32
|
+
|
|
33
|
+
const SOURCE = `---
|
|
34
|
+
title: Metrics Brief
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
# Metrics Brief {[title]}
|
|
38
|
+
|
|
39
|
+
This report collects the strongest signals from a year of product work.
|
|
40
|
+
|
|
41
|
+
## Growth
|
|
42
|
+
|
|
43
|
+
Revenue increased 42% year over year while customer retention reached 91%.
|
|
44
|
+
|
|
45
|
+
## Milestone
|
|
46
|
+
|
|
47
|
+
On January 15, 2026 the team shipped the largest release in company history.
|
|
48
|
+
|
|
49
|
+
## Customer voice
|
|
50
|
+
|
|
51
|
+
> "The new workflow saves our team hours every week."
|
|
52
|
+
`;
|
|
53
|
+
|
|
54
|
+
function TransformButton() {
|
|
55
|
+
const { setSelectedTransformStyle } = usePreviewSettings();
|
|
56
|
+
return <button onClick={() => setSelectedTransformStyle('data-driven')}>Summarize now</button>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function Harness({ children }: { children?: ReactNode }) {
|
|
60
|
+
const { doc } = useEditorContext();
|
|
61
|
+
return (
|
|
62
|
+
<PreviewSettingsProvider doc={doc}>
|
|
63
|
+
<TransformButton />
|
|
64
|
+
{children}
|
|
65
|
+
<PreviewPanel />
|
|
66
|
+
</PreviewSettingsProvider>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function renderMode(displayMode?: string) {
|
|
71
|
+
const source = displayMode
|
|
72
|
+
? SOURCE.replace('title: Metrics Brief', `title: Metrics Brief\ndisplay-mode: ${displayMode}`)
|
|
73
|
+
: SOURCE;
|
|
74
|
+
return render(
|
|
75
|
+
<EditorProvider initialMarkdown={source} initialView="preview">
|
|
76
|
+
<Harness />
|
|
77
|
+
</EditorProvider>,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
describe('PreviewPanel transform projection', () => {
|
|
82
|
+
it('replaces the slideshow deck when a summarization style is selected', async () => {
|
|
83
|
+
renderMode('slideshow');
|
|
84
|
+
await waitFor(() => expect(screen.getByTestId('mock-doc-player')).toBeTruthy());
|
|
85
|
+
expect(screen.getByTestId('mock-doc-player').textContent).not.toContain('transform-');
|
|
86
|
+
|
|
87
|
+
fireEvent.click(screen.getByRole('button', { name: 'Summarize now' }));
|
|
88
|
+
|
|
89
|
+
await waitFor(() =>
|
|
90
|
+
expect(screen.getByTestId('mock-doc-player').textContent).toContain('transform-'),
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('feeds the transformed content model to Page mode', async () => {
|
|
95
|
+
renderMode('page');
|
|
96
|
+
await waitFor(() => expect(screen.getByTestId('mock-linear-doc')).toBeTruthy());
|
|
97
|
+
|
|
98
|
+
fireEvent.click(screen.getByRole('button', { name: 'Summarize now' }));
|
|
99
|
+
|
|
100
|
+
await waitFor(() =>
|
|
101
|
+
expect(screen.getByTestId('mock-linear-doc').textContent).toContain('transform-'),
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('feeds readable transformed Markdown to Document mode', async () => {
|
|
106
|
+
renderMode('document');
|
|
107
|
+
await waitFor(() => expect(screen.getByTestId('mock-plain-document')).toBeTruthy());
|
|
108
|
+
|
|
109
|
+
fireEvent.click(screen.getByRole('button', { name: 'Summarize now' }));
|
|
110
|
+
|
|
111
|
+
await waitFor(() => {
|
|
112
|
+
const markdown = screen.getByTestId('mock-plain-document').textContent ?? '';
|
|
113
|
+
expect(markdown).toContain('Metrics Brief');
|
|
114
|
+
expect(markdown).toContain('42%');
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { canHandleSquisqMediaDrop, ownsMonacoModel } from '../rawEditorIsolation';
|
|
3
|
+
import { SQUISQ_MEDIA_MIME } from '../mediaDragMime';
|
|
4
|
+
|
|
5
|
+
describe('RawEditor Monaco provider isolation', () => {
|
|
6
|
+
it('accepts only the model owned by the registering editor', () => {
|
|
7
|
+
const firstModel = {} as never;
|
|
8
|
+
const secondModel = {} as never;
|
|
9
|
+
const firstEditor = { getModel: () => firstModel } as never;
|
|
10
|
+
expect(ownsMonacoModel(firstEditor, firstModel)).toBe(true);
|
|
11
|
+
expect(ownsMonacoModel(firstEditor, secondModel)).toBe(false);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('rejects media-bin drops while read-only', () => {
|
|
15
|
+
const transfer = { types: [SQUISQ_MEDIA_MIME] } as unknown as DataTransfer;
|
|
16
|
+
expect(canHandleSquisqMediaDrop(true, transfer)).toBe(false);
|
|
17
|
+
expect(canHandleSquisqMediaDrop(false, transfer)).toBe(true);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { DEFAULT_THEME, VIEWPORT_PRESETS } from '@bendyline/squisq/schemas';
|
|
3
|
+
import type { Block, Doc, TextLayer } from '@bendyline/squisq/schemas';
|
|
4
|
+
import { resolveBlockVisual } from '../resolveBlockVisual';
|
|
5
|
+
|
|
6
|
+
describe('resolveBlockVisual', () => {
|
|
7
|
+
it('materializes document-scoped custom templates in editor previews', () => {
|
|
8
|
+
const block: Block = {
|
|
9
|
+
id: 'custom',
|
|
10
|
+
startTime: 0,
|
|
11
|
+
duration: 5,
|
|
12
|
+
audioSegment: 0,
|
|
13
|
+
template: 'hero',
|
|
14
|
+
title: 'Editor custom template',
|
|
15
|
+
};
|
|
16
|
+
const doc: Doc = {
|
|
17
|
+
articleId: 'custom-preview',
|
|
18
|
+
duration: 5,
|
|
19
|
+
blocks: [block],
|
|
20
|
+
audio: { segments: [] },
|
|
21
|
+
customTemplates: [
|
|
22
|
+
{
|
|
23
|
+
name: 'hero',
|
|
24
|
+
label: 'Hero',
|
|
25
|
+
viewport: { width: 1920, height: 1080 },
|
|
26
|
+
layers: [
|
|
27
|
+
{
|
|
28
|
+
id: 'hero-title',
|
|
29
|
+
type: 'text',
|
|
30
|
+
position: { x: '5%', y: '10%', width: '90%' },
|
|
31
|
+
content: { text: '{title}', style: { fontSize: 48, color: '#000000' } },
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const visual = resolveBlockVisual(doc, block, DEFAULT_THEME, VIEWPORT_PRESETS.landscape);
|
|
39
|
+
|
|
40
|
+
expect(visual).not.toBeNull();
|
|
41
|
+
expect((visual?.layers?.[0] as TextLayer).content.text).toBe('Editor custom template');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { EditorProvider } from '../EditorContext';
|
|
4
|
+
import { StatusBar } from '../StatusBar';
|
|
5
|
+
|
|
6
|
+
function renderStatusBar(markdown: string) {
|
|
7
|
+
return render(
|
|
8
|
+
<EditorProvider initialMarkdown={markdown} articleId="status-bar-test">
|
|
9
|
+
<StatusBar />
|
|
10
|
+
</EditorProvider>,
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('StatusBar', () => {
|
|
15
|
+
it('counts nested document blocks, not only top-level roots', async () => {
|
|
16
|
+
renderStatusBar('# Root\n\n## Child one\n\n### Grandchild\n\n## Child two\n');
|
|
17
|
+
|
|
18
|
+
expect(await screen.findByText('4 blocks')).toBeTruthy();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('uses the singular label for one block', async () => {
|
|
22
|
+
renderStatusBar('# Only block\n');
|
|
23
|
+
|
|
24
|
+
expect(await screen.findByText('1 block')).toBeTruthy();
|
|
25
|
+
expect(screen.queryByText('1 blocks')).toBeNull();
|
|
26
|
+
});
|
|
27
|
+
});
|