@bendyline/squisq-editor-react 1.6.2 → 2.0.1
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 +1275 -183
- package/dist/index.js +20345 -12777
- 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 +1607 -241
- package/package.json +9 -5
- package/src/BlockPropertiesPopover.tsx +30 -15
- package/src/DocumentSettingsDialog.tsx +39 -33
- package/src/EditorContext.tsx +199 -42
- package/src/EditorShell.tsx +349 -261
- 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 +362 -89
- package/src/PreviewPanel.tsx +312 -78
- package/src/RawEditor.tsx +13 -2
- package/src/RecorderEntry.tsx +2 -0
- 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 +357 -51
- 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__/codeContextSectionView.test.tsx +8 -6
- package/src/__tests__/customLayoutManagerTheme.test.tsx +34 -0
- package/src/__tests__/documentSettingsDialog.test.tsx +51 -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__/mediaAttachmentFlow.test.ts +2 -2
- 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 +382 -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__/recorderTheme.test.tsx +42 -0
- package/src/__tests__/resolveBlockVisual.test.ts +43 -0
- package/src/__tests__/selectionConversions.test.ts +80 -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 +71 -7
- package/src/__tests__/tiptapCodeBlockRoundTrip.test.ts +109 -0
- package/src/__tests__/tiptapImageRoundTrip.test.ts +1 -1
- package/src/__tests__/toolbarSelectionConversion.test.tsx +164 -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 +254 -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 +284 -0
- package/src/asciiDiagram/__tests__/asciiDiagramOps.test.ts +114 -0
- package/src/asciiDiagram/__tests__/asciiPaste.test.ts +43 -0
- package/src/asciiDiagram/asciiDiagramCommands.ts +174 -0
- package/src/asciiDiagram/asciiDiagramData.ts +149 -0
- package/src/asciiDiagram/asciiDiagramOps.ts +238 -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/codeContext/types.ts +1 -1
- 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/__tests__/useMemoryLayerAdapter.test.ts +13 -0
- package/src/customTemplates/designer.css +158 -99
- package/src/customTemplates/library.ts +16 -3
- package/src/customTemplates/useMemoryLayerAdapter.ts +7 -1
- 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 +25 -6
- package/src/diagram/types.ts +35 -0
- package/src/editorScrollSync.ts +73 -0
- package/src/emojiData.ts +3 -23
- package/src/frontmatterSettings.ts +23 -0
- 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 +198 -20
- 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/RecorderButton.tsx +9 -1
- package/src/recorder/RecorderModal.tsx +84 -41
- package/src/recorder/RecorderPanel.tsx +9 -1
- 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 +119 -0
- package/src/scene/adapters/DiagramAdapter.ts +12 -101
- package/src/scene/adapters/DrawingAdapter.ts +6 -1
- package/src/scene/adapters/LayoutAdapter.ts +3 -0
- package/src/scene/commands/SceneCommand.ts +17 -3
- 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 +39 -23
- package/src/scene/tools/ShapeTool.ts +2 -3
- package/src/selectionConversions.ts +155 -0
- package/src/styles/ascii-diagram.css +79 -0
- package/src/styles/ascii-timeline.css +596 -0
- package/src/styles/editor.css +736 -120
- package/src/styles/index.css +3 -0
- package/src/styles/tree-view.css +189 -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 +861 -0
- package/src/timeline/TimelineViewExtension.ts +252 -0
- package/src/timeline/__tests__/TimelineEditorWidget.test.tsx +414 -0
- package/src/timeline/__tests__/TimelineViewExtension.test.ts +163 -0
- package/src/timeline/__tests__/timelineCommands.test.ts +359 -0
- package/src/timeline/__tests__/timelineOps.test.ts +277 -0
- package/src/timeline/__tests__/timelinePaste.test.ts +34 -0
- package/src/timeline/timelineCommands.ts +421 -0
- package/src/timeline/timelineData.ts +54 -0
- package/src/timeline/timelineOps.ts +381 -0
- package/src/timeline/timelinePaste.ts +8 -0
- package/src/tiptapBridge.ts +47 -21
- package/src/transformStyleId.ts +17 -0
- package/src/treeview/TreeOutlineWidget.tsx +390 -0
- package/src/treeview/TreeViewExtension.ts +250 -0
- package/src/treeview/__tests__/TreeOutlineWidget.test.tsx +156 -0
- package/src/treeview/__tests__/TreeViewExtension.test.ts +122 -0
- package/src/treeview/__tests__/treeOps.test.ts +177 -0
- package/src/treeview/__tests__/treePaste.test.ts +40 -0
- package/src/treeview/__tests__/treeViewCommands.test.ts +179 -0
- package/src/treeview/treeOps.ts +231 -0
- package/src/treeview/treePaste.ts +13 -0
- package/src/treeview/treeViewCommands.ts +99 -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,112 @@ 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
|
+
|
|
91
|
+
function CoverSlideProbe() {
|
|
92
|
+
const { activeCoverSlide, setCoverSlideEnabled } = usePreviewSettings();
|
|
93
|
+
const { markdownSource } = useEditorContext();
|
|
94
|
+
return (
|
|
95
|
+
<>
|
|
96
|
+
<button type="button" onClick={() => setCoverSlideEnabled(false)}>
|
|
97
|
+
Hide cover slide
|
|
98
|
+
</button>
|
|
99
|
+
<button type="button" onClick={() => setCoverSlideEnabled(true)}>
|
|
100
|
+
Use default cover slide
|
|
101
|
+
</button>
|
|
102
|
+
<div data-testid="active-cover-slide">{String(activeCoverSlide)}</div>
|
|
103
|
+
<pre data-testid="markdown-source">{markdownSource}</pre>
|
|
104
|
+
</>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function CoverSlideHarness() {
|
|
109
|
+
const { doc } = useEditorContext();
|
|
110
|
+
return (
|
|
111
|
+
<PreviewSettingsProvider doc={doc}>
|
|
112
|
+
<CoverSlideProbe />
|
|
113
|
+
</PreviewSettingsProvider>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function ManagedDefaultsProbe() {
|
|
118
|
+
const {
|
|
119
|
+
activeThemeId,
|
|
120
|
+
activeTransformStyle,
|
|
121
|
+
activeCaptionStyle,
|
|
122
|
+
activeCaptionsEnabled,
|
|
123
|
+
setSelectedThemeId,
|
|
124
|
+
setSelectedTransformStyle,
|
|
125
|
+
setCaptionMode,
|
|
126
|
+
} = usePreviewSettings();
|
|
127
|
+
const { markdownSource } = useEditorContext();
|
|
128
|
+
return (
|
|
129
|
+
<>
|
|
130
|
+
<button type="button" onClick={() => setSelectedThemeId('standard')}>
|
|
131
|
+
Use default theme
|
|
132
|
+
</button>
|
|
133
|
+
<button type="button" onClick={() => setSelectedTransformStyle('')}>
|
|
134
|
+
Use default transform
|
|
135
|
+
</button>
|
|
136
|
+
<button type="button" onClick={() => setCaptionMode('standard')}>
|
|
137
|
+
Use default captions
|
|
138
|
+
</button>
|
|
139
|
+
<div
|
|
140
|
+
data-testid="managed-defaults"
|
|
141
|
+
data-theme={activeThemeId}
|
|
142
|
+
data-transform={activeTransformStyle}
|
|
143
|
+
data-caption-style={activeCaptionStyle}
|
|
144
|
+
data-captions-enabled={String(activeCaptionsEnabled)}
|
|
145
|
+
/>
|
|
146
|
+
<pre data-testid="markdown-source">{markdownSource}</pre>
|
|
147
|
+
</>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function ManagedDefaultsHarness() {
|
|
152
|
+
const { doc } = useEditorContext();
|
|
153
|
+
return (
|
|
154
|
+
<PreviewSettingsProvider doc={doc}>
|
|
155
|
+
<ManagedDefaultsProbe />
|
|
156
|
+
</PreviewSettingsProvider>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
38
160
|
function renderPreviewControls(markdown: string) {
|
|
39
161
|
render(
|
|
40
162
|
<EditorProvider initialMarkdown={markdown}>
|
|
@@ -51,7 +173,10 @@ function renderPreviewToolbar(markdown: string) {
|
|
|
51
173
|
);
|
|
52
174
|
}
|
|
53
175
|
|
|
54
|
-
afterEach(() =>
|
|
176
|
+
afterEach(() => {
|
|
177
|
+
cleanup();
|
|
178
|
+
clearThemeLibrary();
|
|
179
|
+
});
|
|
55
180
|
|
|
56
181
|
describe('PreviewModeSwitch', () => {
|
|
57
182
|
it('labels the plain document preview as Document and the styled view as Page', () => {
|
|
@@ -62,7 +187,8 @@ describe('PreviewModeSwitch', () => {
|
|
|
62
187
|
.map((button) => button.textContent)
|
|
63
188
|
.filter(Boolean);
|
|
64
189
|
|
|
65
|
-
expect(labels).toEqual(['
|
|
190
|
+
expect(labels).toEqual(['Slideshow', 'Video', 'Page', 'Document', 'Narrate']);
|
|
191
|
+
expect(screen.getByTestId('active-mode').textContent).toBe('slideshow');
|
|
66
192
|
|
|
67
193
|
fireEvent.click(screen.getByRole('button', { name: 'Document' }));
|
|
68
194
|
expect(screen.getByTestId('active-mode').textContent).toBe('page');
|
|
@@ -89,9 +215,178 @@ describe('PreviewModeSwitch', () => {
|
|
|
89
215
|
});
|
|
90
216
|
expect(screen.getByRole('button', { name: 'Page' }).getAttribute('aria-pressed')).toBe('true');
|
|
91
217
|
});
|
|
218
|
+
|
|
219
|
+
it('uses the shared squisq-theme, themeId, theme fallback order', async () => {
|
|
220
|
+
renderPreviewControls('---\nthemeId: bold\ntheme: cinematic\n---\n\n# Hello');
|
|
221
|
+
|
|
222
|
+
await waitFor(() => {
|
|
223
|
+
expect(screen.getByTestId('active-mode').getAttribute('data-theme-id')).toBe('bold');
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('canonicalizes the stored dataDriven transform id', async () => {
|
|
228
|
+
renderPreviewControls('---\nsquisq-transform: dataDriven\n---\n\n# Hello');
|
|
229
|
+
|
|
230
|
+
await waitFor(() => {
|
|
231
|
+
expect(screen.getByTestId('active-mode').getAttribute('data-transform-style')).toBe(
|
|
232
|
+
'data-driven',
|
|
233
|
+
);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('copies a selected browser-library theme into the document atomically', async () => {
|
|
238
|
+
saveLibraryTheme(
|
|
239
|
+
compileTheme({
|
|
240
|
+
id: 'library-theme',
|
|
241
|
+
name: 'Library Theme',
|
|
242
|
+
seedColors: { primary: '#456789' },
|
|
243
|
+
}),
|
|
244
|
+
);
|
|
245
|
+
render(
|
|
246
|
+
<EditorProvider initialMarkdown="# Hello">
|
|
247
|
+
<LibraryThemeHarness />
|
|
248
|
+
</EditorProvider>,
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
fireEvent.click(screen.getByRole('button', { name: 'Select library theme' }));
|
|
252
|
+
|
|
253
|
+
await waitFor(() => {
|
|
254
|
+
expect(screen.getByTestId('active-theme-name').textContent).toBe('Library Theme');
|
|
255
|
+
const source = screen.getByTestId('markdown-source').textContent ?? '';
|
|
256
|
+
expect(source).toContain('squisq-theme: library-theme');
|
|
257
|
+
expect(source).toContain('squisq-custom-themes:');
|
|
258
|
+
expect(source).toContain('library-theme');
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
describe('cover-slide frontmatter', () => {
|
|
264
|
+
it('writes the non-default as a boolean and removes the default value', async () => {
|
|
265
|
+
render(
|
|
266
|
+
<EditorProvider initialMarkdown="# Hello">
|
|
267
|
+
<CoverSlideHarness />
|
|
268
|
+
</EditorProvider>,
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
expect(screen.getByTestId('active-cover-slide').textContent).toBe('true');
|
|
272
|
+
fireEvent.click(screen.getByRole('button', { name: 'Hide cover slide' }));
|
|
273
|
+
|
|
274
|
+
await waitFor(() => {
|
|
275
|
+
const source = screen.getByTestId('markdown-source').textContent ?? '';
|
|
276
|
+
expect(source).toContain('squisq-cover-slide: false');
|
|
277
|
+
expect(source).not.toContain('squisq-cover-slide: "false"');
|
|
278
|
+
});
|
|
279
|
+
expect(screen.getByTestId('active-cover-slide').textContent).toBe('false');
|
|
280
|
+
|
|
281
|
+
fireEvent.click(screen.getByRole('button', { name: 'Use default cover slide' }));
|
|
282
|
+
|
|
283
|
+
await waitFor(() => {
|
|
284
|
+
const source = screen.getByTestId('markdown-source').textContent ?? '';
|
|
285
|
+
expect(source).not.toContain('squisq-cover-slide');
|
|
286
|
+
expect(source).not.toContain('---');
|
|
287
|
+
});
|
|
288
|
+
expect(screen.getByTestId('active-cover-slide').textContent).toBe('true');
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('removes a legacy cover-slide override when restoring the default', async () => {
|
|
292
|
+
render(
|
|
293
|
+
<EditorProvider initialMarkdown={'---\ncover-slide: false\ntitle: Hello\n---\n\n# Hello'}>
|
|
294
|
+
<CoverSlideHarness />
|
|
295
|
+
</EditorProvider>,
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
await waitFor(() => {
|
|
299
|
+
expect(screen.getByTestId('active-cover-slide').textContent).toBe('false');
|
|
300
|
+
});
|
|
301
|
+
fireEvent.click(screen.getByRole('button', { name: 'Use default cover slide' }));
|
|
302
|
+
|
|
303
|
+
await waitFor(() => {
|
|
304
|
+
const source = screen.getByTestId('markdown-source').textContent ?? '';
|
|
305
|
+
expect(source).not.toContain('cover-slide');
|
|
306
|
+
expect(source).toContain('title: Hello');
|
|
307
|
+
});
|
|
308
|
+
expect(screen.getByTestId('active-cover-slide').textContent).toBe('true');
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
describe('managed preview-setting defaults', () => {
|
|
313
|
+
it('removes default theme, transform, and caption values plus their legacy aliases', async () => {
|
|
314
|
+
const markdown = `---
|
|
315
|
+
squisq-theme: documentary
|
|
316
|
+
themeId: bold
|
|
317
|
+
theme: cinematic
|
|
318
|
+
squisq-transform: documentary
|
|
319
|
+
transform-style: magazine
|
|
320
|
+
squisq-captions: social
|
|
321
|
+
caption-style: off
|
|
322
|
+
title: Hello
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
# Hello`;
|
|
326
|
+
render(
|
|
327
|
+
<EditorProvider initialMarkdown={markdown}>
|
|
328
|
+
<ManagedDefaultsHarness />
|
|
329
|
+
</EditorProvider>,
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
fireEvent.click(screen.getByRole('button', { name: 'Use default theme' }));
|
|
333
|
+
await waitFor(() => {
|
|
334
|
+
const source = screen.getByTestId('markdown-source').textContent ?? '';
|
|
335
|
+
expect(source).not.toMatch(/^(?:squisq-theme|themeId|theme):/m);
|
|
336
|
+
});
|
|
337
|
+
expect(screen.getByTestId('managed-defaults').getAttribute('data-theme')).toBe('standard');
|
|
338
|
+
|
|
339
|
+
fireEvent.click(screen.getByRole('button', { name: 'Use default transform' }));
|
|
340
|
+
await waitFor(() => {
|
|
341
|
+
const source = screen.getByTestId('markdown-source').textContent ?? '';
|
|
342
|
+
expect(source).not.toMatch(/^(?:squisq-transform|transform-style):/m);
|
|
343
|
+
});
|
|
344
|
+
expect(screen.getByTestId('managed-defaults').getAttribute('data-transform')).toBe('');
|
|
345
|
+
|
|
346
|
+
fireEvent.click(screen.getByRole('button', { name: 'Use default captions' }));
|
|
347
|
+
await waitFor(() => {
|
|
348
|
+
const source = screen.getByTestId('markdown-source').textContent ?? '';
|
|
349
|
+
expect(source).not.toMatch(/^(?:squisq-captions|caption-style):/m);
|
|
350
|
+
expect(source).toContain('title: Hello');
|
|
351
|
+
});
|
|
352
|
+
const defaults = screen.getByTestId('managed-defaults');
|
|
353
|
+
expect(defaults.getAttribute('data-caption-style')).toBe('standard');
|
|
354
|
+
expect(defaults.getAttribute('data-captions-enabled')).toBe('true');
|
|
355
|
+
});
|
|
92
356
|
});
|
|
93
357
|
|
|
94
358
|
describe('PreviewToolbarControls', () => {
|
|
359
|
+
it('presents transforms as summarization without implying the source is changed', () => {
|
|
360
|
+
const originalResizeObserver = globalThis.ResizeObserver;
|
|
361
|
+
class ResizeObserverStub implements ResizeObserver {
|
|
362
|
+
observe() {}
|
|
363
|
+
unobserve() {}
|
|
364
|
+
disconnect() {}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
globalThis.ResizeObserver = ResizeObserverStub;
|
|
368
|
+
try {
|
|
369
|
+
renderPreviewToolbar('# Hello');
|
|
370
|
+
|
|
371
|
+
const summarizeLabels = screen.getAllByText('Summarize:');
|
|
372
|
+
expect(summarizeLabels.length).toBeGreaterThan(0);
|
|
373
|
+
for (const label of summarizeLabels) {
|
|
374
|
+
expect(label.getAttribute('title')).toBe(
|
|
375
|
+
'Extract and summarize content for presentation with these Use modes. Your underlying content is not changed.',
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
expect(screen.queryByText('Transform:')).toBeNull();
|
|
379
|
+
expect(document.querySelector('[role="group"][aria-label="Display mode"]')).toBeNull();
|
|
380
|
+
expect(document.querySelector('[role="group"][aria-label="Aspect ratio"]')).not.toBeNull();
|
|
381
|
+
} finally {
|
|
382
|
+
if (originalResizeObserver) {
|
|
383
|
+
globalThis.ResizeObserver = originalResizeObserver;
|
|
384
|
+
} else {
|
|
385
|
+
Reflect.deleteProperty(globalThis, 'ResizeObserver');
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
|
|
95
390
|
it('keeps the overflow popover inside the left viewport edge', async () => {
|
|
96
391
|
const originalResizeObserver = globalThis.ResizeObserver;
|
|
97
392
|
const originalGetBoundingClientRect = HTMLElement.prototype.getBoundingClientRect;
|
|
@@ -161,3 +456,85 @@ describe('PreviewToolbarControls', () => {
|
|
|
161
456
|
}
|
|
162
457
|
});
|
|
163
458
|
});
|
|
459
|
+
|
|
460
|
+
describe('Use tab mode menu', () => {
|
|
461
|
+
it('lists the Use modes beside the tab and selects a mode without a toolbar radio group', () => {
|
|
462
|
+
render(
|
|
463
|
+
<EditorProvider initialMarkdown="# Hello" initialView="wysiwyg" allowRecording={false}>
|
|
464
|
+
<UseTabHarness />
|
|
465
|
+
</EditorProvider>,
|
|
466
|
+
);
|
|
467
|
+
|
|
468
|
+
const useTab = screen.getByRole('tab', { name: 'Slideshow' });
|
|
469
|
+
expect(screen.queryByRole('tab', { name: 'Use' })).toBeNull();
|
|
470
|
+
const tabGroup = useTab.parentElement;
|
|
471
|
+
expect(tabGroup).not.toBeNull();
|
|
472
|
+
expect(within(tabGroup!).getByRole('button', { name: 'Choose Use mode' })).toBeTruthy();
|
|
473
|
+
|
|
474
|
+
// The first click enters Use; once active, clicking the tab again opens
|
|
475
|
+
// the mode menu.
|
|
476
|
+
fireEvent.click(useTab);
|
|
477
|
+
expect(screen.getByTestId('active-view').textContent).toBe('preview');
|
|
478
|
+
expect(screen.queryByRole('menu', { name: 'Use mode' })).toBeNull();
|
|
479
|
+
|
|
480
|
+
fireEvent.click(screen.getByRole('tab', { name: 'Slideshow' }));
|
|
481
|
+
|
|
482
|
+
const menu = screen.getByRole('menu', { name: 'Use mode' });
|
|
483
|
+
expect(
|
|
484
|
+
within(menu)
|
|
485
|
+
.getAllByRole('menuitemradio')
|
|
486
|
+
.map((item) => item.querySelector('.squisq-use-mode-menu-label')?.textContent),
|
|
487
|
+
).toEqual(['Slideshow', 'Video', 'Page', 'Document', 'Narrate']);
|
|
488
|
+
const slideshowItem = within(menu).getByRole('menuitemradio', { name: 'Slideshow' });
|
|
489
|
+
expect(slideshowItem.getAttribute('aria-checked')).toBe('true');
|
|
490
|
+
expect(within(slideshowItem).getByText('Present designed slides one at a time.')).toBeTruthy();
|
|
491
|
+
|
|
492
|
+
fireEvent.click(within(menu).getByRole('menuitemradio', { name: 'Document' }));
|
|
493
|
+
expect(screen.getByTestId('active-mode').textContent).toBe('page');
|
|
494
|
+
expect(screen.getByTestId('active-view').textContent).toBe('preview');
|
|
495
|
+
expect(screen.getByRole('tab', { name: 'Document' })).toBeTruthy();
|
|
496
|
+
expect(screen.queryByRole('menu', { name: 'Use mode' })).toBeNull();
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
describe('Narrate mode gating', () => {
|
|
501
|
+
function renderWithNarrateGate(markdown: string, allowNarrate: boolean) {
|
|
502
|
+
render(
|
|
503
|
+
<EditorProvider initialMarkdown={markdown} allowNarrate={allowNarrate}>
|
|
504
|
+
<PreviewHarness />
|
|
505
|
+
</EditorProvider>,
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
it('selects narrate from the switch and resolves teleprompter frontmatter aliases', async () => {
|
|
510
|
+
renderPreviewControls('# Hello');
|
|
511
|
+
fireEvent.click(screen.getByRole('button', { name: 'Narrate' }));
|
|
512
|
+
expect(screen.getByTestId('active-mode').textContent).toBe('narrate');
|
|
513
|
+
|
|
514
|
+
cleanup();
|
|
515
|
+
renderPreviewControls('---\ndisplay-mode: teleprompter\n---\n\n# Hello');
|
|
516
|
+
await waitFor(() => {
|
|
517
|
+
expect(screen.getByTestId('active-mode').textContent).toBe('narrate');
|
|
518
|
+
});
|
|
519
|
+
expect(screen.getByRole('button', { name: 'Narrate' }).getAttribute('aria-pressed')).toBe(
|
|
520
|
+
'true',
|
|
521
|
+
);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
it('allowNarrate=false hides the button and clamps frontmatter-forced narrate to video', async () => {
|
|
525
|
+
renderWithNarrateGate('---\ndisplay-mode: narrate\n---\n\n# Hello', false);
|
|
526
|
+
|
|
527
|
+
expect(screen.queryByRole('button', { name: 'Narrate' })).toBeNull();
|
|
528
|
+
// Give the frontmatter parse a tick, then confirm the clamp held.
|
|
529
|
+
await waitFor(() => {
|
|
530
|
+
expect(screen.getByTestId('active-mode').textContent).toBe('video');
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
it('allowNarrate=true (default) honors frontmatter narrate', async () => {
|
|
535
|
+
renderWithNarrateGate('---\ndisplay-mode: narrate\n---\n\n# Hello', true);
|
|
536
|
+
await waitFor(() => {
|
|
537
|
+
expect(screen.getByTestId('active-mode').textContent).toBe('narrate');
|
|
538
|
+
});
|
|
539
|
+
});
|
|
540
|
+
});
|
|
@@ -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
|
+
});
|