@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
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/** @vitest-environment jsdom */
|
|
2
|
+
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import type { ContentContainer } from '@bendyline/squisq/storage';
|
|
5
|
+
import { MemoryContentContainer } from '@bendyline/squisq/storage';
|
|
6
|
+
import type { MediaProvider } from '@bendyline/squisq/schemas';
|
|
7
|
+
import { useEditorContext } from '../EditorContext';
|
|
8
|
+
|
|
9
|
+
const imageEditorProps: Array<{
|
|
10
|
+
filesContainer: ContentContainer;
|
|
11
|
+
saveFormat: 'png' | 'jpeg' | 'webp';
|
|
12
|
+
onExport: (blob: Blob, format: 'png' | 'jpeg' | 'webp') => void;
|
|
13
|
+
}> = [];
|
|
14
|
+
|
|
15
|
+
vi.mock('../ImageEditor', () => ({
|
|
16
|
+
ImageEditor: (props: (typeof imageEditorProps)[number]) => {
|
|
17
|
+
imageEditorProps.push(props);
|
|
18
|
+
return <div data-testid="image-editor-contract-stub" />;
|
|
19
|
+
},
|
|
20
|
+
}));
|
|
21
|
+
vi.mock('../RawEditor', () => ({ RawEditor: () => <div /> }));
|
|
22
|
+
vi.mock('../WysiwygEditor', () => ({ WysiwygEditor: () => <div /> }));
|
|
23
|
+
vi.mock('../PreviewPanel', () => ({ PreviewPanel: () => <div /> }));
|
|
24
|
+
|
|
25
|
+
import { EditorShell } from '../EditorShell';
|
|
26
|
+
|
|
27
|
+
function Trigger({ path }: { path: string }) {
|
|
28
|
+
const { openImageEdit } = useEditorContext();
|
|
29
|
+
return <button onClick={() => openImageEdit(path)}>edit</button>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function provider(): MediaProvider {
|
|
33
|
+
return {
|
|
34
|
+
addMedia: vi.fn(async (name: string) => name),
|
|
35
|
+
resolveUrl: vi.fn(async (name: string) => `blob:${name}`),
|
|
36
|
+
listMedia: vi.fn(async () => []),
|
|
37
|
+
removeMedia: vi.fn(async () => {}),
|
|
38
|
+
dispose: vi.fn(),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
imageEditorProps.length = 0;
|
|
44
|
+
if (typeof window.matchMedia !== 'function') {
|
|
45
|
+
Object.defineProperty(window, 'matchMedia', {
|
|
46
|
+
configurable: true,
|
|
47
|
+
value: () => ({ matches: false, addEventListener() {}, removeEventListener() {} }),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('ImageEditModal storage contract', () => {
|
|
53
|
+
it('writes JPEG bytes/MIME back to a .jpg path', async () => {
|
|
54
|
+
const mediaProvider = provider();
|
|
55
|
+
render(
|
|
56
|
+
<EditorShell
|
|
57
|
+
initialMarkdown="# image"
|
|
58
|
+
mediaProvider={mediaProvider}
|
|
59
|
+
toolbarSlotRight={<Trigger path="photo.jpg" />}
|
|
60
|
+
/>,
|
|
61
|
+
);
|
|
62
|
+
fireEvent.click(screen.getByText('edit'));
|
|
63
|
+
await waitFor(() => expect(screen.getByTestId('image-editor-contract-stub')).toBeTruthy());
|
|
64
|
+
const props = imageEditorProps[imageEditorProps.length - 1]!;
|
|
65
|
+
expect(props.saveFormat).toBe('jpeg');
|
|
66
|
+
await act(async () => props.onExport(new Blob(['jpeg']), 'jpeg'));
|
|
67
|
+
expect(mediaProvider.addMedia).toHaveBeenCalledWith(
|
|
68
|
+
'photo.jpg',
|
|
69
|
+
expect.any(Blob),
|
|
70
|
+
'image/jpeg',
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('hashes sidecar paths so sanitized-name collisions remain separate', async () => {
|
|
75
|
+
const parent = new MemoryContentContainer();
|
|
76
|
+
const mediaProvider = provider();
|
|
77
|
+
const first = render(
|
|
78
|
+
<EditorShell
|
|
79
|
+
initialMarkdown="# image"
|
|
80
|
+
workspaceContainer={parent}
|
|
81
|
+
mediaProvider={mediaProvider}
|
|
82
|
+
toolbarSlotRight={<Trigger path="a/b.png" />}
|
|
83
|
+
/>,
|
|
84
|
+
);
|
|
85
|
+
fireEvent.click(screen.getByText('edit'));
|
|
86
|
+
await waitFor(() => expect(imageEditorProps).toHaveLength(1));
|
|
87
|
+
const firstContainer = imageEditorProps[0].filesContainer;
|
|
88
|
+
first.unmount();
|
|
89
|
+
|
|
90
|
+
render(
|
|
91
|
+
<EditorShell
|
|
92
|
+
initialMarkdown="# image"
|
|
93
|
+
workspaceContainer={parent}
|
|
94
|
+
mediaProvider={mediaProvider}
|
|
95
|
+
toolbarSlotRight={<Trigger path="a_b.png" />}
|
|
96
|
+
/>,
|
|
97
|
+
);
|
|
98
|
+
fireEvent.click(screen.getByText('edit'));
|
|
99
|
+
await waitFor(() => expect(imageEditorProps).toHaveLength(2));
|
|
100
|
+
const secondContainer = imageEditorProps[1].filesContainer;
|
|
101
|
+
await firstContainer.writeFile('sentinel', new Uint8Array([1]).buffer);
|
|
102
|
+
await secondContainer.writeFile('sentinel', new Uint8Array([2]).buffer);
|
|
103
|
+
const paths = (await parent.listFiles('.imageEdits')).map((entry) => entry.path);
|
|
104
|
+
expect(paths.filter((path) => path.endsWith('/sentinel'))).toHaveLength(2);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('refuses unsupported source formats instead of writing mismatched bytes', async () => {
|
|
108
|
+
render(
|
|
109
|
+
<EditorShell
|
|
110
|
+
initialMarkdown="# image"
|
|
111
|
+
mediaProvider={provider()}
|
|
112
|
+
toolbarSlotRight={<Trigger path="animated.gif" />}
|
|
113
|
+
/>,
|
|
114
|
+
);
|
|
115
|
+
fireEvent.click(screen.getByText('edit'));
|
|
116
|
+
await waitFor(() => expect(screen.getByText(/supports PNG, JPEG, and WebP/i)).toBeTruthy());
|
|
117
|
+
expect(screen.queryByTestId('image-editor-contract-stub')).toBeNull();
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* renders the toolbar / canvas / panels.
|
|
7
7
|
*/
|
|
8
8
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
9
|
-
import { render, screen, waitFor } from '@testing-library/react';
|
|
9
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
10
10
|
import { MemoryContentContainer, scopeContainer } from '@bendyline/squisq/storage';
|
|
11
11
|
import { writeImageEditDoc, createEmptyImageEditDoc } from '@bendyline/squisq/imageEdit';
|
|
12
12
|
import { DARK_SURFACE } from '@bendyline/squisq/schemas';
|
|
@@ -56,6 +56,20 @@ describe('<ImageEditor>', () => {
|
|
|
56
56
|
expect(screen.getByText(/loading image editor/i)).toBeTruthy();
|
|
57
57
|
});
|
|
58
58
|
|
|
59
|
+
it('adds a centered shape layer from the Layers panel menu', async () => {
|
|
60
|
+
const parent = new MemoryContentContainer();
|
|
61
|
+
const sidecar = scopeContainer(parent, 'pic_files');
|
|
62
|
+
await writeImageEditDoc(sidecar, createEmptyImageEditDoc(640, 480));
|
|
63
|
+
|
|
64
|
+
render(<ImageEditor filesContainer={sidecar} />);
|
|
65
|
+
await waitFor(() => expect(screen.getByTestId('image-editor')).toBeTruthy());
|
|
66
|
+
|
|
67
|
+
fireEvent.click(screen.getByRole('button', { name: 'Add layer' }));
|
|
68
|
+
fireEvent.click(screen.getByRole('menuitem', { name: 'Shape layer' }));
|
|
69
|
+
|
|
70
|
+
await waitFor(() => expect(screen.getByTitle('Rectangle')).toBeTruthy());
|
|
71
|
+
});
|
|
72
|
+
|
|
59
73
|
it('applies an explicit dark surface to the editor chrome', async () => {
|
|
60
74
|
const parent = new MemoryContentContainer();
|
|
61
75
|
const sidecar = scopeContainer(parent, 'pic_files');
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from 'vitest';
|
|
2
2
|
import { useState } from 'react';
|
|
3
|
-
import { fireEvent, render, screen } from '@testing-library/react';
|
|
3
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
4
4
|
import type { SquisqAnnotatedSchema } from '@bendyline/squisq/jsonForm';
|
|
5
5
|
import { JsonEditor } from '../jsonEditor';
|
|
6
6
|
|
|
@@ -165,4 +165,59 @@ describe('JsonEditor', () => {
|
|
|
165
165
|
fireEvent.change(input, { target: { value: 'changed' } });
|
|
166
166
|
expect(onChange).not.toHaveBeenCalled();
|
|
167
167
|
});
|
|
168
|
+
|
|
169
|
+
it('escapes object property names as RFC 6901 pointer segments', () => {
|
|
170
|
+
const schema: SquisqAnnotatedSchema = {
|
|
171
|
+
type: 'object',
|
|
172
|
+
properties: { 'a/b~c': { type: 'string', title: 'Odd key' } },
|
|
173
|
+
};
|
|
174
|
+
const onChange = vi.fn();
|
|
175
|
+
render(<Controlled schema={schema} initial={{ 'a/b~c': 'before' }} onValueChange={onChange} />);
|
|
176
|
+
fireEvent.change(screen.getByLabelText('Odd key'), { target: { value: 'after' } });
|
|
177
|
+
expect(onChange).toHaveBeenLastCalledWith({ 'a/b~c': 'after' });
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('associates labels/help/errors and reports validation after render', async () => {
|
|
181
|
+
const schema: SquisqAnnotatedSchema = {
|
|
182
|
+
type: 'object',
|
|
183
|
+
properties: {
|
|
184
|
+
title: { type: 'string', title: 'Title', description: 'A useful title' },
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
const onValidate = vi.fn();
|
|
188
|
+
render(
|
|
189
|
+
<JsonEditor
|
|
190
|
+
schema={schema}
|
|
191
|
+
value={{ title: '' }}
|
|
192
|
+
onChange={() => {}}
|
|
193
|
+
validate={() => [{ path: '/title', message: 'Required' }]}
|
|
194
|
+
onValidate={onValidate}
|
|
195
|
+
/>,
|
|
196
|
+
);
|
|
197
|
+
const input = screen.getByLabelText('Title');
|
|
198
|
+
expect(input.getAttribute('aria-invalid')).toBe('true');
|
|
199
|
+
const described = input.getAttribute('aria-describedby') ?? '';
|
|
200
|
+
expect(described).toContain('-help');
|
|
201
|
+
expect(described).toContain('-error');
|
|
202
|
+
expect(screen.getByRole('alert').textContent).toBe('Required');
|
|
203
|
+
await waitFor(() =>
|
|
204
|
+
expect(onValidate).toHaveBeenCalledWith([{ path: '/title', message: 'Required' }]),
|
|
205
|
+
);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('adds ARIA state and keyboard navigation to segmented/tabs controls', () => {
|
|
209
|
+
const schema: SquisqAnnotatedSchema = {
|
|
210
|
+
oneOf: [
|
|
211
|
+
{ type: 'string', title: 'Text option' },
|
|
212
|
+
{ type: 'number', title: 'Number option' },
|
|
213
|
+
],
|
|
214
|
+
squisq: { control: 'tabs', label: 'Value type' },
|
|
215
|
+
};
|
|
216
|
+
render(<JsonEditor schema={schema} value="hello" onChange={() => {}} />);
|
|
217
|
+
const tabs = screen.getAllByRole('tab');
|
|
218
|
+
expect(tabs[0].getAttribute('aria-selected')).toBe('true');
|
|
219
|
+
fireEvent.keyDown(tabs[0], { key: 'ArrowRight' });
|
|
220
|
+
expect(tabs[1].getAttribute('aria-selected')).toBe('true');
|
|
221
|
+
expect(screen.getByRole('tabpanel')).toBeTruthy();
|
|
222
|
+
});
|
|
168
223
|
});
|
|
@@ -7,6 +7,8 @@ import type { ImageEditDoc } from '@bendyline/squisq/schemas';
|
|
|
7
7
|
import { LayersPanel } from '../imageEditor/LayersPanel.js';
|
|
8
8
|
import type { ImageEditorAction } from '../imageEditor/state.js';
|
|
9
9
|
|
|
10
|
+
const ignoreAddLayer = () => {};
|
|
11
|
+
|
|
10
12
|
function buildDoc(): ImageEditDoc {
|
|
11
13
|
return {
|
|
12
14
|
version: 1,
|
|
@@ -41,7 +43,14 @@ function buildDoc(): ImageEditDoc {
|
|
|
41
43
|
describe('LayersPanel', () => {
|
|
42
44
|
it('renders layers with the top of the SVG stack first', () => {
|
|
43
45
|
const dispatch = vi.fn<(a: ImageEditorAction) => void>();
|
|
44
|
-
render(
|
|
46
|
+
render(
|
|
47
|
+
<LayersPanel
|
|
48
|
+
doc={buildDoc()}
|
|
49
|
+
selectedLayerId={null}
|
|
50
|
+
dispatch={dispatch}
|
|
51
|
+
onAddLayer={ignoreAddLayer}
|
|
52
|
+
/>,
|
|
53
|
+
);
|
|
45
54
|
const items = screen.getAllByText(/Bottom|Middle|Top/);
|
|
46
55
|
expect(items[0]?.textContent).toContain('Top');
|
|
47
56
|
expect(items[items.length - 1]?.textContent).toContain('Bottom');
|
|
@@ -49,14 +58,60 @@ describe('LayersPanel', () => {
|
|
|
49
58
|
|
|
50
59
|
it('clicking the layer name dispatches a select action', () => {
|
|
51
60
|
const dispatch = vi.fn<(a: ImageEditorAction) => void>();
|
|
52
|
-
render(
|
|
61
|
+
render(
|
|
62
|
+
<LayersPanel
|
|
63
|
+
doc={buildDoc()}
|
|
64
|
+
selectedLayerId={null}
|
|
65
|
+
dispatch={dispatch}
|
|
66
|
+
onAddLayer={ignoreAddLayer}
|
|
67
|
+
/>,
|
|
68
|
+
);
|
|
53
69
|
fireEvent.click(screen.getByText('Middle'));
|
|
54
70
|
expect(dispatch).toHaveBeenCalledWith({ type: 'select', layerId: 'b' });
|
|
55
71
|
});
|
|
56
72
|
|
|
73
|
+
it('exposes the full layer name as a tooltip when the row is truncated', () => {
|
|
74
|
+
const dispatch = vi.fn<(a: ImageEditorAction) => void>();
|
|
75
|
+
render(
|
|
76
|
+
<LayersPanel
|
|
77
|
+
doc={buildDoc()}
|
|
78
|
+
selectedLayerId={null}
|
|
79
|
+
dispatch={dispatch}
|
|
80
|
+
onAddLayer={ignoreAddLayer}
|
|
81
|
+
/>,
|
|
82
|
+
);
|
|
83
|
+
expect(screen.getByTitle('Middle').textContent).toContain('Middle');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('opens the add-layer menu and reports the selected layer kind', () => {
|
|
87
|
+
const dispatch = vi.fn<(a: ImageEditorAction) => void>();
|
|
88
|
+
const onAddLayer = vi.fn();
|
|
89
|
+
render(
|
|
90
|
+
<LayersPanel
|
|
91
|
+
doc={buildDoc()}
|
|
92
|
+
selectedLayerId={null}
|
|
93
|
+
dispatch={dispatch}
|
|
94
|
+
onAddLayer={onAddLayer}
|
|
95
|
+
/>,
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
fireEvent.click(screen.getByRole('button', { name: 'Add layer' }));
|
|
99
|
+
expect(screen.getByRole('menu')).toBeTruthy();
|
|
100
|
+
fireEvent.click(screen.getByRole('menuitem', { name: 'Text layer' }));
|
|
101
|
+
expect(onAddLayer).toHaveBeenCalledWith('text');
|
|
102
|
+
expect(screen.queryByRole('menu')).toBeNull();
|
|
103
|
+
});
|
|
104
|
+
|
|
57
105
|
it('toggling visibility dispatches an update-layer action', () => {
|
|
58
106
|
const dispatch = vi.fn<(a: ImageEditorAction) => void>();
|
|
59
|
-
render(
|
|
107
|
+
render(
|
|
108
|
+
<LayersPanel
|
|
109
|
+
doc={buildDoc()}
|
|
110
|
+
selectedLayerId={null}
|
|
111
|
+
dispatch={dispatch}
|
|
112
|
+
onAddLayer={ignoreAddLayer}
|
|
113
|
+
/>,
|
|
114
|
+
);
|
|
60
115
|
// The "Hide layer" buttons exist for visible layers.
|
|
61
116
|
const hideButtons = screen.getAllByRole('button', { name: 'Hide layer' });
|
|
62
117
|
fireEvent.click(hideButtons[0]!); // top-most visible layer is 'b'
|
|
@@ -69,7 +124,14 @@ describe('LayersPanel', () => {
|
|
|
69
124
|
|
|
70
125
|
it('move-up button is disabled at the top of the stack', () => {
|
|
71
126
|
const dispatch = vi.fn<(a: ImageEditorAction) => void>();
|
|
72
|
-
render(
|
|
127
|
+
render(
|
|
128
|
+
<LayersPanel
|
|
129
|
+
doc={buildDoc()}
|
|
130
|
+
selectedLayerId={null}
|
|
131
|
+
dispatch={dispatch}
|
|
132
|
+
onAddLayer={ignoreAddLayer}
|
|
133
|
+
/>,
|
|
134
|
+
);
|
|
73
135
|
const upButtons = screen.getAllByRole('button', { name: 'Move layer up' });
|
|
74
136
|
// Visual order is c, b, a; layer 'c' is at the top of the stack and cannot move up.
|
|
75
137
|
expect((upButtons[0] as HTMLButtonElement).disabled).toBe(true);
|
|
@@ -77,7 +139,14 @@ describe('LayersPanel', () => {
|
|
|
77
139
|
|
|
78
140
|
it('delete button dispatches a remove-layer action', () => {
|
|
79
141
|
const dispatch = vi.fn<(a: ImageEditorAction) => void>();
|
|
80
|
-
render(
|
|
142
|
+
render(
|
|
143
|
+
<LayersPanel
|
|
144
|
+
doc={buildDoc()}
|
|
145
|
+
selectedLayerId={null}
|
|
146
|
+
dispatch={dispatch}
|
|
147
|
+
onAddLayer={ignoreAddLayer}
|
|
148
|
+
/>,
|
|
149
|
+
);
|
|
81
150
|
const delButtons = screen.getAllByRole('button', { name: 'Delete layer' });
|
|
82
151
|
fireEvent.click(delButtons[0]!); // top of stack ('c')
|
|
83
152
|
expect(dispatch).toHaveBeenCalledWith({ type: 'remove-layer', layerId: 'c' });
|
|
@@ -90,6 +159,7 @@ describe('LayersPanel', () => {
|
|
|
90
159
|
doc={{ version: 1, canvas: { width: 1, height: 1 }, layers: [] }}
|
|
91
160
|
selectedLayerId={null}
|
|
92
161
|
dispatch={dispatch}
|
|
162
|
+
onAddLayer={ignoreAddLayer}
|
|
93
163
|
/>,
|
|
94
164
|
);
|
|
95
165
|
expect(screen.getByText(/no layers yet/i)).toBeTruthy();
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { DiagramEdge, DiagramNode, EditorShellProps } from '../index';
|
|
3
|
+
|
|
4
|
+
// @ts-expect-error React Flow-prefixed types were removed in the major API cleanup.
|
|
5
|
+
import type { DiagramRFNode } from '../index';
|
|
6
|
+
|
|
7
|
+
describe('major-version editor API types', () => {
|
|
8
|
+
it('uses implementation-neutral diagram names', () => {
|
|
9
|
+
const node: DiagramNode = { id: 'a', position: { x: 0, y: 0 }, data: { label: 'A' } };
|
|
10
|
+
const edge: DiagramEdge = { id: 'a-b', source: 'a', target: 'b' };
|
|
11
|
+
expect([node.id, edge.id]).toEqual(['a', 'a-b']);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('does not accept the removed EditorShell container prop', () => {
|
|
15
|
+
const props: EditorShellProps = {
|
|
16
|
+
// @ts-expect-error Use workspaceContainer; container was removed.
|
|
17
|
+
container: null,
|
|
18
|
+
};
|
|
19
|
+
expect(props).toHaveProperty('container');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('keeps the old type import absent', () => {
|
|
23
|
+
const legacy: DiagramRFNode | null = null;
|
|
24
|
+
expect(legacy).toBeNull();
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
6
|
+
import type { MediaEntry, MediaProvider } from '@bendyline/squisq/schemas';
|
|
7
|
+
import { MediaBin } from '../MediaBin.js';
|
|
8
|
+
|
|
9
|
+
function createProvider(): { provider: MediaProvider; added: string[] } {
|
|
10
|
+
const entries: MediaEntry[] = [];
|
|
11
|
+
const added: string[] = [];
|
|
12
|
+
return {
|
|
13
|
+
added,
|
|
14
|
+
provider: {
|
|
15
|
+
async addMedia(name, data, mimeType) {
|
|
16
|
+
const relativePath = `attachments/${name}`;
|
|
17
|
+
added.push(relativePath);
|
|
18
|
+
entries.push({
|
|
19
|
+
name: relativePath,
|
|
20
|
+
mimeType,
|
|
21
|
+
size: data instanceof Blob ? data.size : data.byteLength,
|
|
22
|
+
});
|
|
23
|
+
return relativePath;
|
|
24
|
+
},
|
|
25
|
+
async resolveUrl(path) {
|
|
26
|
+
return `blob:${path}`;
|
|
27
|
+
},
|
|
28
|
+
async listMedia() {
|
|
29
|
+
return [...entries];
|
|
30
|
+
},
|
|
31
|
+
async removeMedia() {},
|
|
32
|
+
dispose() {},
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function dataTransfer(files: File[], itemTypes: string[]) {
|
|
38
|
+
return {
|
|
39
|
+
files,
|
|
40
|
+
items: itemTypes.map((type) => ({ kind: 'file', type })),
|
|
41
|
+
types: ['Files'],
|
|
42
|
+
dropEffect: 'none',
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe('MediaBin image drop target', () => {
|
|
47
|
+
it('shows a drop affordance and uploads a dropped image through the normal callback', async () => {
|
|
48
|
+
const { provider, added } = createProvider();
|
|
49
|
+
const onMediaUploaded = vi.fn();
|
|
50
|
+
const image = new File([new Uint8Array([1, 2, 3])], 'photo.png', { type: 'image/png' });
|
|
51
|
+
Object.defineProperty(image, 'arrayBuffer', {
|
|
52
|
+
configurable: true,
|
|
53
|
+
value: async () => new Uint8Array([1, 2, 3]).buffer,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const { container } = render(
|
|
57
|
+
<MediaBin mediaProvider={provider} isDark onMediaUploaded={onMediaUploaded} />,
|
|
58
|
+
);
|
|
59
|
+
const bin = container.querySelector('.squisq-media-bin') as HTMLElement;
|
|
60
|
+
const transfer = dataTransfer([image], ['image/png']);
|
|
61
|
+
await screen.findByText('No files yet.');
|
|
62
|
+
|
|
63
|
+
fireEvent.dragEnter(bin, { dataTransfer: transfer });
|
|
64
|
+
expect(bin.classList.contains('squisq-media-bin--drop-active')).toBe(true);
|
|
65
|
+
expect(screen.getByText('Drop images here')).toBeTruthy();
|
|
66
|
+
|
|
67
|
+
fireEvent.drop(bin, { dataTransfer: transfer });
|
|
68
|
+
|
|
69
|
+
await waitFor(() => expect(added).toEqual(['attachments/photo.png']));
|
|
70
|
+
expect(onMediaUploaded).toHaveBeenCalledWith('attachments/photo.png', 'photo.png', 'image/png');
|
|
71
|
+
expect(await screen.findByText('photo.png')).toBeTruthy();
|
|
72
|
+
expect(screen.queryByText('Drop images here')).toBeNull();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('does not activate or upload for a non-image file', async () => {
|
|
76
|
+
const { provider, added } = createProvider();
|
|
77
|
+
const text = new File(['notes'], 'notes.txt', { type: 'text/plain' });
|
|
78
|
+
const { container } = render(<MediaBin mediaProvider={provider} isDark={false} />);
|
|
79
|
+
const bin = container.querySelector('.squisq-media-bin') as HTMLElement;
|
|
80
|
+
const transfer = dataTransfer([text], ['text/plain']);
|
|
81
|
+
await screen.findByText('No files yet.');
|
|
82
|
+
|
|
83
|
+
fireEvent.dragEnter(bin, { dataTransfer: transfer });
|
|
84
|
+
fireEvent.drop(bin, { dataTransfer: transfer });
|
|
85
|
+
|
|
86
|
+
expect(bin.classList.contains('squisq-media-bin--drop-active')).toBe(false);
|
|
87
|
+
expect(screen.queryByText('Drop images here')).toBeNull();
|
|
88
|
+
await waitFor(() => expect(added).toEqual([]));
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
import { parseMarkdown } from '@bendyline/squisq/markdown';
|
|
6
|
+
import { markdownToDoc } from '@bendyline/squisq/doc';
|
|
7
|
+
import {
|
|
8
|
+
buildNarrationScript,
|
|
9
|
+
parseNarrationTimingJson,
|
|
10
|
+
type NarrationAlignment,
|
|
11
|
+
} from '@bendyline/squisq/narration';
|
|
12
|
+
import {
|
|
13
|
+
insertNarrationPreamble,
|
|
14
|
+
narrationAnnotationLine,
|
|
15
|
+
} from '../teleprompter/recording/insertPreamble';
|
|
16
|
+
import { buildNarrationSavePlan } from '../teleprompter/recording/narrationSave';
|
|
17
|
+
|
|
18
|
+
describe('insertNarrationPreamble', () => {
|
|
19
|
+
it('inserts at the top of a plain document', () => {
|
|
20
|
+
const next = insertNarrationPreamble('# Title\n\nBody.\n', 'audio/narration-1.webm', null);
|
|
21
|
+
expect(next.startsWith('{[audio src=audio/narration-1.webm anchor=document]}\n\n# Title')).toBe(
|
|
22
|
+
true,
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('inserts after the frontmatter fence', () => {
|
|
27
|
+
const source = '---\ntitle: Doc\n---\n\n# Title\n\nBody.\n';
|
|
28
|
+
const next = insertNarrationPreamble(source, 'audio/narration-1.webm', null);
|
|
29
|
+
const lines = next.split('\n');
|
|
30
|
+
expect(lines[2]).toBe('---');
|
|
31
|
+
expect(next).toContain(
|
|
32
|
+
'---\n\n{[audio src=audio/narration-1.webm anchor=document]}\n\n# Title',
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('replaces a previous take (and its camera line) on retake', () => {
|
|
37
|
+
const withTake = insertNarrationPreamble(
|
|
38
|
+
'# Title\n\nBody.\n',
|
|
39
|
+
'audio/narration-old.webm',
|
|
40
|
+
'video/narration-cam-old.webm',
|
|
41
|
+
);
|
|
42
|
+
expect(withTake).toContain('narration-old');
|
|
43
|
+
expect(withTake).toContain('<video src="video/narration-cam-old.webm"');
|
|
44
|
+
|
|
45
|
+
const retaken = insertNarrationPreamble(withTake, 'audio/narration-new.webm', null);
|
|
46
|
+
expect(retaken).not.toContain('narration-old');
|
|
47
|
+
expect(retaken).not.toContain('narration-cam-old');
|
|
48
|
+
expect(retaken).toContain('{[audio src=audio/narration-new.webm anchor=document]}');
|
|
49
|
+
// No duplicate annotations.
|
|
50
|
+
expect(retaken.match(/\{\[audio /g)?.length).toBe(1);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('adds the camera companion as an inline video line', () => {
|
|
54
|
+
const next = insertNarrationPreamble(
|
|
55
|
+
'# Title\n\nBody.\n',
|
|
56
|
+
'audio/narration-1.webm',
|
|
57
|
+
'video/narration-cam-1.webm',
|
|
58
|
+
);
|
|
59
|
+
expect(next).toContain('{[audio src=audio/narration-1.webm anchor=document]}');
|
|
60
|
+
expect(next).toContain('<video src="video/narration-cam-1.webm" controls width="240"></video>');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('quotes srcs with spaces', () => {
|
|
64
|
+
expect(narrationAnnotationLine('audio/my take.webm')).toBe(
|
|
65
|
+
'{[audio src="audio/my take.webm" anchor=document]}',
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('round-trips through markdownToDoc as documentMedia', () => {
|
|
70
|
+
const next = insertNarrationPreamble(
|
|
71
|
+
'---\ntitle: Doc\n---\n\n# Title\n\nBody.\n',
|
|
72
|
+
'audio/narration-1.webm',
|
|
73
|
+
null,
|
|
74
|
+
);
|
|
75
|
+
const doc = markdownToDoc(parseMarkdown(next));
|
|
76
|
+
expect(doc.documentMedia?.length).toBe(1);
|
|
77
|
+
expect(doc.documentMedia![0].src).toBe('audio/narration-1.webm');
|
|
78
|
+
expect(doc.documentMedia![0].anchor).toBe('document');
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('buildNarrationSavePlan', () => {
|
|
83
|
+
const script = buildNarrationScript(
|
|
84
|
+
markdownToDoc(parseMarkdown('# One\n\nAlpha beta.\n\n# Two\n\nGamma delta.\n')),
|
|
85
|
+
);
|
|
86
|
+
const alignment: NarrationAlignment = {
|
|
87
|
+
words: script.tokens.map((_, i) => ({ tokenIndex: i, tSec: i * 0.4, interpolated: false })),
|
|
88
|
+
blocks: script.blocks.map((range, i) => ({
|
|
89
|
+
blockId: range.blockId,
|
|
90
|
+
blockIndex: i,
|
|
91
|
+
charStart: range.charStart,
|
|
92
|
+
charEnd: range.charEnd,
|
|
93
|
+
startSec: i * 5,
|
|
94
|
+
endSec: (i + 1) * 5,
|
|
95
|
+
})),
|
|
96
|
+
detectedSyllables: script.totalSyllables,
|
|
97
|
+
cost: 0,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
it('produces recorder-convention paths and a v3 sidecar the parser accepts', () => {
|
|
101
|
+
const plan = buildNarrationSavePlan({
|
|
102
|
+
script,
|
|
103
|
+
alignment,
|
|
104
|
+
durationSec: 10,
|
|
105
|
+
audioExt: '.webm',
|
|
106
|
+
cameraExt: '.webm',
|
|
107
|
+
baseWpm: 150,
|
|
108
|
+
cameraOffsetSec: 0.02,
|
|
109
|
+
});
|
|
110
|
+
expect(plan.audioRelativeName).toMatch(/^audio\/narration-.*\.webm$/);
|
|
111
|
+
expect(plan.cameraRelativeName).toMatch(/^video\/narration-cam.*\.webm$/);
|
|
112
|
+
expect(plan.sidecarPathFor('audio/renamed.webm')).toBe('audio/renamed.webm.timing.json');
|
|
113
|
+
|
|
114
|
+
const parsed = parseNarrationTimingJson(JSON.stringify(plan.sidecarPayload));
|
|
115
|
+
expect(parsed).not.toBeNull();
|
|
116
|
+
expect(parsed!.blocks.length).toBe(script.blocks.length);
|
|
117
|
+
expect(parsed!.bookmarks.length).toBe(script.tokens.length);
|
|
118
|
+
expect(parsed!.cameraOffsetSec).toBeCloseTo(0.02, 6);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('degrades to an empty-timing sidecar when alignment failed', () => {
|
|
122
|
+
const plan = buildNarrationSavePlan({
|
|
123
|
+
script,
|
|
124
|
+
alignment: null,
|
|
125
|
+
durationSec: 10,
|
|
126
|
+
audioExt: '.webm',
|
|
127
|
+
cameraExt: null,
|
|
128
|
+
baseWpm: 150,
|
|
129
|
+
});
|
|
130
|
+
expect(plan.cameraRelativeName).toBeNull();
|
|
131
|
+
expect(plan.sidecarPayload.blocks).toEqual([]);
|
|
132
|
+
expect(plan.sidecarPayload.bookmarks).toEqual([]);
|
|
133
|
+
expect(plan.sidecarPayload.sourceText).toBe(script.sourceText);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('nextMarkdown composes the preamble from one snapshot', () => {
|
|
137
|
+
const plan = buildNarrationSavePlan({
|
|
138
|
+
script,
|
|
139
|
+
alignment,
|
|
140
|
+
durationSec: 10,
|
|
141
|
+
audioExt: '.webm',
|
|
142
|
+
cameraExt: null,
|
|
143
|
+
baseWpm: 150,
|
|
144
|
+
});
|
|
145
|
+
const next = plan.nextMarkdown('# One\n\nAlpha beta.\n', 'audio/narration-x.webm', null);
|
|
146
|
+
expect(next).toContain('{[audio src=audio/narration-x.webm anchor=document]}');
|
|
147
|
+
});
|
|
148
|
+
});
|