@bendyline/squisq-editor-react 2.0.1 → 2.2.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/LICENSE +21 -0
- package/NOTICE.md +40 -0
- package/README.md +10 -2
- package/dist/index.d.ts +683 -60
- package/dist/index.js +9648 -3750
- package/dist/monaco.js +0 -1
- package/dist/styles/index.css +1160 -241
- package/package.json +10 -6
- package/dist/index.js.map +0 -1
- package/dist/monaco.js.map +0 -1
- package/src/BlockCardView.tsx +0 -121
- package/src/BlockPreviewPanel.tsx +0 -69
- package/src/BlockPropertiesPopover.tsx +0 -222
- package/src/DocumentSettingsDialog.tsx +0 -272
- package/src/DropZoneOverlay.tsx +0 -137
- package/src/EditorContext.tsx +0 -1247
- package/src/EditorShell.tsx +0 -1334
- package/src/EmojiPicker.tsx +0 -332
- package/src/FolderView.tsx +0 -131
- package/src/Icon.tsx +0 -26
- package/src/ImageEditor.tsx +0 -395
- package/src/ImageNodeView.tsx +0 -284
- package/src/ImageViewer.tsx +0 -221
- package/src/InlineIcon.ts +0 -84
- package/src/InlinePreviewGutter.tsx +0 -590
- package/src/LinkDialog.tsx +0 -276
- package/src/MediaBin.tsx +0 -500
- package/src/MentionExtension.tsx +0 -261
- package/src/OutlinePanel.tsx +0 -547
- package/src/PlainHtmlPreview.tsx +0 -277
- package/src/PreviewControls.tsx +0 -1250
- package/src/PreviewPanel.tsx +0 -422
- package/src/RawEditor.tsx +0 -731
- package/src/RecorderEntry.tsx +0 -159
- package/src/StatusBar.tsx +0 -51
- package/src/TemplateAnnotation.ts +0 -167
- package/src/TemplateContentPreview.tsx +0 -56
- package/src/TemplatePicker.tsx +0 -1314
- package/src/ThemeCustomizerPanel.tsx +0 -304
- package/src/ThemePicker.tsx +0 -433
- package/src/TimelineBlockPreview.tsx +0 -37
- package/src/TimelineTrack.tsx +0 -671
- package/src/Toolbar.tsx +0 -2587
- package/src/Tooltip.tsx +0 -142
- package/src/TransitionPicker.tsx +0 -390
- package/src/VersionHistoryPanel.tsx +0 -359
- package/src/ViewMenuPanel.tsx +0 -189
- package/src/ViewSwitcher.tsx +0 -56
- package/src/WysiwygEditor.tsx +0 -792
- package/src/__tests__/blockProperties.test.ts +0 -92
- package/src/__tests__/blockPropertiesPopoverTheme.test.tsx +0 -33
- package/src/__tests__/blockRange.test.ts +0 -105
- package/src/__tests__/blockTagActivity.test.ts +0 -183
- package/src/__tests__/buildDocumentPreviewMarkdown.test.ts +0 -75
- package/src/__tests__/buildPreviewDocContent.test.ts +0 -48
- package/src/__tests__/buildPreviewDocTransition.test.ts +0 -92
- package/src/__tests__/canvasSurfaceTextEditing.test.tsx +0 -60
- package/src/__tests__/codeContextSectionView.test.tsx +0 -97
- package/src/__tests__/codeContextZoneManager.test.ts +0 -127
- package/src/__tests__/createShapeLayer.test.ts +0 -46
- package/src/__tests__/customLayoutManagerTheme.test.tsx +0 -34
- package/src/__tests__/detectMarkdown.test.ts +0 -73
- package/src/__tests__/diffContextSections.test.ts +0 -39
- package/src/__tests__/documentSettingsDialog.test.tsx +0 -197
- package/src/__tests__/drawingShapeRoundTrip.test.ts +0 -49
- package/src/__tests__/editorScrollHandoff.test.tsx +0 -102
- package/src/__tests__/editorScrollSync.test.ts +0 -65
- package/src/__tests__/editorShellCodeContext.test.tsx +0 -86
- package/src/__tests__/editorShellProps.test.tsx +0 -485
- package/src/__tests__/embeddedMedia.test.ts +0 -48
- package/src/__tests__/emojiPicker.test.tsx +0 -93
- package/src/__tests__/fileKind.test.ts +0 -112
- package/src/__tests__/headingTransition.test.ts +0 -176
- package/src/__tests__/imageEditAffordance.test.tsx +0 -272
- package/src/__tests__/imageEditModalContract.test.tsx +0 -119
- package/src/__tests__/imageEditorShell.test.tsx +0 -94
- package/src/__tests__/imageEditorState.test.ts +0 -171
- package/src/__tests__/inlinePreviewGutter.test.tsx +0 -62
- package/src/__tests__/inlinePreviewGutterAllBlocks.test.tsx +0 -103
- package/src/__tests__/jsonEditor.test.tsx +0 -223
- package/src/__tests__/layersPanel.test.tsx +0 -167
- package/src/__tests__/layoutChildRoundTrip.test.ts +0 -71
- package/src/__tests__/linkDialogDocPicker.test.tsx +0 -137
- package/src/__tests__/majorApiTypes.test.ts +0 -26
- package/src/__tests__/mediaAttachmentFlow.test.ts +0 -110
- package/src/__tests__/mediaBinDrop.test.tsx +0 -90
- package/src/__tests__/mediaReferences.test.ts +0 -82
- package/src/__tests__/narrationSave.test.ts +0 -148
- package/src/__tests__/outlinePanel.test.tsx +0 -169
- package/src/__tests__/outlineSource.test.ts +0 -217
- package/src/__tests__/plainHtmlPreview.test.tsx +0 -125
- package/src/__tests__/presentationMode.test.tsx +0 -290
- package/src/__tests__/previewControls.test.tsx +0 -540
- package/src/__tests__/previewPanelAudioFailure.test.tsx +0 -46
- package/src/__tests__/previewPanelPresentation.test.tsx +0 -126
- package/src/__tests__/previewPanelTransforms.test.tsx +0 -117
- package/src/__tests__/propertiesPanel.test.tsx +0 -69
- package/src/__tests__/rawEditorModelIsolation.test.ts +0 -19
- package/src/__tests__/recorderFormats.test.ts +0 -146
- package/src/__tests__/recorderMediaInsert.test.ts +0 -86
- package/src/__tests__/recorderTheme.test.tsx +0 -42
- package/src/__tests__/recorderTimingJson.test.ts +0 -41
- package/src/__tests__/resolveBlockVisual.test.ts +0 -43
- package/src/__tests__/selectionConversions.test.ts +0 -80
- package/src/__tests__/statusBar.test.tsx +0 -27
- package/src/__tests__/teleprompter.test.tsx +0 -190
- package/src/__tests__/templateAnnotationRoundTrip.test.ts +0 -99
- package/src/__tests__/templateContentPreview.test.ts +0 -101
- package/src/__tests__/templatePickerMetadata.test.ts +0 -32
- package/src/__tests__/templatePickerPortal.test.tsx +0 -62
- package/src/__tests__/timelineSource.test.ts +0 -134
- package/src/__tests__/tiptapBridge.test.ts +0 -566
- package/src/__tests__/tiptapBridgeConformance.test.ts +0 -47
- package/src/__tests__/tiptapCodeBlockRoundTrip.test.ts +0 -109
- package/src/__tests__/tiptapImageRoundTrip.test.ts +0 -73
- package/src/__tests__/toolbarSelectionConversion.test.tsx +0 -164
- package/src/__tests__/tooltip.test.tsx +0 -72
- package/src/__tests__/transformStyleId.test.ts +0 -13
- package/src/__tests__/transitionCatalog.test.ts +0 -64
- package/src/__tests__/useBlockNavigator.test.tsx +0 -67
- package/src/__tests__/useImageEditor.test.tsx +0 -226
- package/src/__tests__/useJsonEditorTokens.test.ts +0 -59
- package/src/__tests__/useMediaRecorder.test.ts +0 -302
- package/src/__tests__/useTimelineClock.test.ts +0 -21
- package/src/__tests__/versionHistory.test.tsx +0 -197
- package/src/__tests__/viewMenuPanel.test.tsx +0 -130
- package/src/__tests__/wysiwygImageUpload.test.ts +0 -69
- package/src/__tests__/wysiwygTemplateBadgeFocus.test.tsx +0 -56
- package/src/asciiDiagram/AsciiDiagramExtension.ts +0 -343
- package/src/asciiDiagram/AsciiDiagramWidget.tsx +0 -254
- package/src/asciiDiagram/RepairableDiagramExtension.ts +0 -200
- package/src/asciiDiagram/__tests__/AsciiDiagramExtension.test.ts +0 -145
- package/src/asciiDiagram/__tests__/RepairableDiagramExtension.test.ts +0 -159
- package/src/asciiDiagram/__tests__/asciiDiagramCommands.test.ts +0 -284
- package/src/asciiDiagram/__tests__/asciiDiagramOps.test.ts +0 -114
- package/src/asciiDiagram/__tests__/asciiPaste.test.ts +0 -43
- package/src/asciiDiagram/asciiDiagramCommands.ts +0 -174
- package/src/asciiDiagram/asciiDiagramData.ts +0 -149
- package/src/asciiDiagram/asciiDiagramOps.ts +0 -238
- package/src/asciiDiagram/asciiPaste.ts +0 -19
- package/src/blockProperties.ts +0 -88
- package/src/blockRange.ts +0 -132
- package/src/blockSlice.ts +0 -75
- package/src/blockTagActivity.ts +0 -233
- package/src/buildDocumentPreviewMarkdown.ts +0 -168
- package/src/buildPreviewDoc.ts +0 -405
- package/src/codeContext/CodeContextSectionView.tsx +0 -124
- package/src/codeContext/CodeContextZoneManager.ts +0 -149
- package/src/codeContext/CodeContextZones.tsx +0 -121
- package/src/codeContext/diffContextSections.ts +0 -38
- package/src/codeContext/types.ts +0 -75
- package/src/customTemplates/AddBin.tsx +0 -126
- package/src/customTemplates/CustomLayoutManager.tsx +0 -234
- package/src/customTemplates/CustomTemplateContext.tsx +0 -188
- package/src/customTemplates/LayerToolbar.tsx +0 -580
- package/src/customTemplates/ShapeGlyph.tsx +0 -47
- package/src/customTemplates/TemplateDesigner.tsx +0 -433
- package/src/customTemplates/__tests__/library.test.ts +0 -96
- package/src/customTemplates/__tests__/normalizePositions.test.ts +0 -109
- package/src/customTemplates/__tests__/shapeDefs.test.ts +0 -49
- package/src/customTemplates/__tests__/useMemoryLayerAdapter.test.ts +0 -108
- package/src/customTemplates/designer.css +0 -732
- package/src/customTemplates/index.ts +0 -31
- package/src/customTemplates/library.ts +0 -110
- package/src/customTemplates/normalizePositions.ts +0 -75
- package/src/customTemplates/shapeDefs.ts +0 -131
- package/src/customTemplates/thumbnail.tsx +0 -63
- package/src/customTemplates/tokenDefs.ts +0 -60
- package/src/customTemplates/useDocCustomTemplates.ts +0 -52
- package/src/customTemplates/useMemoryLayerAdapter.ts +0 -129
- package/src/customThemes/CustomThemeContext.tsx +0 -185
- package/src/customThemes/CustomThemeDialog.tsx +0 -316
- package/src/customThemes/ImportThemeSection.tsx +0 -178
- package/src/customThemes/__tests__/CustomThemeContext.test.tsx +0 -64
- package/src/customThemes/__tests__/CustomThemeDialog.test.tsx +0 -47
- package/src/customThemes/__tests__/customThemeLibrary.test.ts +0 -60
- package/src/customThemes/__tests__/importThemeSection.test.tsx +0 -192
- package/src/customThemes/customThemeLibrary.ts +0 -100
- package/src/customThemes/index.ts +0 -37
- package/src/customThemes/themeControls.tsx +0 -229
- package/src/customThemes/themeDraft.ts +0 -288
- package/src/customThemes/useDocCustomThemes.ts +0 -49
- package/src/detectMarkdown.ts +0 -62
- package/src/diagram/DiagramCanvas.tsx +0 -259
- package/src/diagram/DiagramMaximizedOverlay.tsx +0 -46
- package/src/diagram/diagramCommands.ts +0 -604
- package/src/diagram/diagramConstants.ts +0 -17
- package/src/diagram/types.ts +0 -35
- package/src/editorScrollSync.ts +0 -73
- package/src/embeddedMedia.ts +0 -78
- package/src/emojiData.ts +0 -1317
- package/src/fileKind.ts +0 -158
- package/src/frontmatter.ts +0 -29
- package/src/frontmatterSettings.ts +0 -23
- package/src/headingTransition.ts +0 -291
- package/src/hooks/useFileDrop.ts +0 -266
- package/src/imageEditor/CanvasSurface.tsx +0 -703
- package/src/imageEditor/ImageVersionHistoryDropdown.tsx +0 -396
- package/src/imageEditor/LayersPanel.tsx +0 -218
- package/src/imageEditor/PropertiesPanel.tsx +0 -474
- package/src/imageEditor/Toolbar.tsx +0 -459
- package/src/imageEditor/createShapeLayer.ts +0 -280
- package/src/imageEditor/icons.tsx +0 -68
- package/src/imageEditor/image-editor.css +0 -574
- package/src/imageEditor/layers/EditorImageLayer.tsx +0 -45
- package/src/imageEditor/layers/EditorShapeLayer.tsx +0 -62
- package/src/imageEditor/layers/EditorTextLayer.tsx +0 -45
- package/src/imageEditor/layers/SelectionHandles.tsx +0 -86
- package/src/imageEditor/state.ts +0 -173
- package/src/imageEditor/useImageEditor.ts +0 -385
- package/src/imageEditor/useImageEditorTokens.ts +0 -70
- package/src/index.ts +0 -470
- package/src/jsonEditor/EmbeddedRichTextField.tsx +0 -81
- package/src/jsonEditor/JsonEditor.tsx +0 -81
- package/src/jsonEditor/JsonEditorContext.tsx +0 -81
- package/src/jsonEditor/RenderNode.tsx +0 -87
- package/src/jsonEditor/editors.tsx +0 -745
- package/src/jsonEditor/index.ts +0 -2
- package/src/jsonEditor/json-editor.css +0 -463
- package/src/jsonEditor/useJsonEditorTokens.ts +0 -33
- package/src/mediaDragMime.ts +0 -32
- package/src/mediaEntries.ts +0 -12
- package/src/mediaReferences.ts +0 -299
- package/src/monaco.ts +0 -35
- package/src/monacoWorkers.ts +0 -89
- package/src/outlineSource.ts +0 -171
- package/src/presentation/PresentationMode.tsx +0 -596
- package/src/rawEditorIsolation.ts +0 -18
- package/src/recorder/RecorderButton.tsx +0 -80
- package/src/recorder/RecorderModal.tsx +0 -706
- package/src/recorder/RecorderPanel.tsx +0 -77
- package/src/recorder/formats.ts +0 -159
- package/src/recorder/hooks/useMediaRecorder.ts +0 -436
- package/src/recorder/hooks/useStreamPreview.ts +0 -47
- package/src/recorder/insertMediaBlock.ts +0 -30
- package/src/recorder/sources/cameraStream.ts +0 -32
- package/src/recorder/sources/micStream.ts +0 -25
- package/src/recorder/sources/screenStream.ts +0 -162
- package/src/recorder/timingJson.ts +0 -66
- package/src/resolveBlockVisual.ts +0 -39
- package/src/scene/Scene.tsx +0 -616
- package/src/scene/SceneBlockExtension.ts +0 -205
- package/src/scene/SceneBlockToolbar.tsx +0 -201
- package/src/scene/SceneBlockWidget.tsx +0 -441
- package/src/scene/ScenePropsBar.tsx +0 -85
- package/src/scene/SceneSelection.tsx +0 -111
- package/src/scene/SceneSideToolbar.tsx +0 -89
- package/src/scene/SceneViewport.tsx +0 -106
- package/src/scene/ShapePalette.tsx +0 -196
- package/src/scene/__tests__/DiagramAdapter.test.ts +0 -142
- package/src/scene/__tests__/SceneBlockExtension.test.ts +0 -33
- package/src/scene/__tests__/bezierEdit.test.ts +0 -85
- package/src/scene/__tests__/blockLayers.test.ts +0 -57
- package/src/scene/__tests__/sceneIsolation.test.tsx +0 -119
- package/src/scene/__tests__/shapeLayers.test.ts +0 -106
- package/src/scene/__tests__/useSceneHitTest.test.ts +0 -90
- package/src/scene/__tests__/useScenePanZoom.test.ts +0 -103
- package/src/scene/adapters/DiagramAdapter.ts +0 -79
- package/src/scene/adapters/DrawingAdapter.ts +0 -420
- package/src/scene/adapters/LayoutAdapter.ts +0 -313
- package/src/scene/adapters/blockLayers.ts +0 -159
- package/src/scene/commands/SceneCommand.ts +0 -84
- package/src/scene/commands/drawingCommands.ts +0 -318
- package/src/scene/commands/layoutCommands.ts +0 -301
- package/src/scene/hooks/useSceneHitTest.ts +0 -105
- package/src/scene/hooks/useScenePanZoom.ts +0 -147
- package/src/scene/hooks/useSceneSelection.ts +0 -62
- package/src/scene/index.ts +0 -90
- package/src/scene/layers/DiagramEdges.tsx +0 -142
- package/src/scene/layers/edgeGeometry.ts +0 -104
- package/src/scene/layers/nodeCard.tsx +0 -164
- package/src/scene/layers/renderLayer.tsx +0 -70
- package/src/scene/layers/shapeLayers.ts +0 -201
- package/src/scene/paths/bezierEdit.ts +0 -208
- package/src/scene/scene.css +0 -792
- package/src/scene/text/SceneTextOverlay.tsx +0 -161
- package/src/scene/text/sceneTextChannel.ts +0 -46
- package/src/scene/text/sceneTextConfig.ts +0 -31
- package/src/scene/text/sceneTiptap.ts +0 -36
- package/src/scene/text/useSceneTextEditing.ts +0 -39
- package/src/scene/tools/ConnectTool.ts +0 -209
- package/src/scene/tools/DrawingConnectTool.ts +0 -231
- package/src/scene/tools/PathTool.ts +0 -158
- package/src/scene/tools/PlaceTool.ts +0 -47
- package/src/scene/tools/SceneTool.ts +0 -85
- package/src/scene/tools/SelectTool.ts +0 -300
- package/src/scene/tools/ShapeTool.ts +0 -143
- package/src/scene/tools/TextTool.ts +0 -72
- package/src/scene/tools/TokenTool.ts +0 -95
- package/src/scene/tools/createDrawShapeTool.ts +0 -82
- package/src/selectionConversions.ts +0 -155
- package/src/styles/ascii-diagram.css +0 -79
- package/src/styles/ascii-timeline.css +0 -596
- package/src/styles/code-context.css +0 -155
- package/src/styles/diagram.css +0 -183
- package/src/styles/editor.css +0 -6811
- package/src/styles/folder-view.css +0 -210
- package/src/styles/image-edit-affordance.css +0 -201
- package/src/styles/index.css +0 -18
- package/src/styles/tree-view.css +0 -189
- package/src/teleprompter/TeleprompterControls.tsx +0 -218
- package/src/teleprompter/TeleprompterSelfView.tsx +0 -22
- package/src/teleprompter/TeleprompterSurface.tsx +0 -267
- package/src/teleprompter/TeleprompterView.tsx +0 -338
- package/src/teleprompter/canvasRenderer.ts +0 -161
- package/src/teleprompter/floatingWindow.ts +0 -352
- package/src/teleprompter/index.ts +0 -61
- package/src/teleprompter/pcmWorklet.ts +0 -62
- package/src/teleprompter/recording/insertPreamble.ts +0 -80
- package/src/teleprompter/recording/narrationSave.ts +0 -134
- package/src/teleprompter/recording/useNarrationRecorder.ts +0 -367
- package/src/teleprompter/scrollModel.ts +0 -85
- package/src/teleprompter/teleprompterTheme.ts +0 -303
- package/src/teleprompter/types.ts +0 -38
- package/src/teleprompter/useFloatingWindow.ts +0 -74
- package/src/teleprompter/useMicAnalysis.ts +0 -211
- package/src/teleprompter/useTeleprompter.ts +0 -421
- package/src/templateContentPreviewResolver.ts +0 -360
- package/src/timeline/TimelineEditorWidget.tsx +0 -861
- package/src/timeline/TimelineViewExtension.ts +0 -252
- package/src/timeline/__tests__/TimelineEditorWidget.test.tsx +0 -414
- package/src/timeline/__tests__/TimelineViewExtension.test.ts +0 -163
- package/src/timeline/__tests__/timelineCommands.test.ts +0 -359
- package/src/timeline/__tests__/timelineOps.test.ts +0 -277
- package/src/timeline/__tests__/timelinePaste.test.ts +0 -34
- package/src/timeline/timelineCommands.ts +0 -421
- package/src/timeline/timelineData.ts +0 -54
- package/src/timeline/timelineOps.ts +0 -381
- package/src/timeline/timelinePaste.ts +0 -8
- package/src/timelineSource.ts +0 -244
- package/src/tiptap/TiptapAudio.tsx +0 -86
- package/src/tiptap/TiptapVideo.tsx +0 -119
- package/src/tiptap/useResolvedMediaSrc.ts +0 -47
- package/src/tiptapBridge.ts +0 -986
- package/src/tooltipPlacement.ts +0 -13
- package/src/transformStyleId.ts +0 -17
- package/src/transitionCatalog.ts +0 -159
- package/src/treeview/TreeOutlineWidget.tsx +0 -390
- package/src/treeview/TreeViewExtension.ts +0 -250
- package/src/treeview/__tests__/TreeOutlineWidget.test.tsx +0 -156
- package/src/treeview/__tests__/TreeViewExtension.test.ts +0 -122
- package/src/treeview/__tests__/treeOps.test.ts +0 -177
- package/src/treeview/__tests__/treePaste.test.ts +0 -40
- package/src/treeview/__tests__/treeViewCommands.test.ts +0 -179
- package/src/treeview/treeOps.ts +0 -231
- package/src/treeview/treePaste.ts +0 -13
- package/src/treeview/treeViewCommands.ts +0 -99
- package/src/treeview/treeViewData.ts +0 -41
- package/src/types/monaco-shims.d.ts +0 -10
- package/src/useBlockNavigator.ts +0 -153
- package/src/useHeadingLayout.ts +0 -294
- package/src/useMonacoLoader.ts +0 -82
- package/src/useTimelineClock.ts +0 -76
- package/src/utils/collectInlineFontAwesomeCss.ts +0 -69
- package/src/utils/dropUtils.ts +0 -136
- package/src/utils/normalizeMalformedAssetUrl.ts +0 -22
- package/src/wysiwygImageUpload.ts +0 -113
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @vitest-environment jsdom
|
|
3
|
-
*/
|
|
4
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
5
|
-
import { fireEvent, render, screen } from '@testing-library/react';
|
|
6
|
-
import type { ImageEditDoc } from '@bendyline/squisq/schemas';
|
|
7
|
-
import { LayersPanel } from '../imageEditor/LayersPanel.js';
|
|
8
|
-
import type { ImageEditorAction } from '../imageEditor/state.js';
|
|
9
|
-
|
|
10
|
-
const ignoreAddLayer = () => {};
|
|
11
|
-
|
|
12
|
-
function buildDoc(): ImageEditDoc {
|
|
13
|
-
return {
|
|
14
|
-
version: 1,
|
|
15
|
-
canvas: { width: 100, height: 100 },
|
|
16
|
-
layers: [
|
|
17
|
-
{
|
|
18
|
-
id: 'a',
|
|
19
|
-
type: 'shape',
|
|
20
|
-
name: 'Bottom',
|
|
21
|
-
position: { x: 0, y: 0, width: 10, height: 10 },
|
|
22
|
-
content: { shape: 'rect' },
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
id: 'b',
|
|
26
|
-
type: 'text',
|
|
27
|
-
name: 'Middle',
|
|
28
|
-
position: { x: 0, y: 0, width: 50, height: 20 },
|
|
29
|
-
content: { text: 'Hi', style: { fontSize: 16, color: '#000' } },
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
id: 'c',
|
|
33
|
-
type: 'shape',
|
|
34
|
-
name: 'Top',
|
|
35
|
-
visible: false,
|
|
36
|
-
position: { x: 0, y: 0, width: 10, height: 10 },
|
|
37
|
-
content: { shape: 'rect' },
|
|
38
|
-
},
|
|
39
|
-
],
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
describe('LayersPanel', () => {
|
|
44
|
-
it('renders layers with the top of the SVG stack first', () => {
|
|
45
|
-
const dispatch = vi.fn<(a: ImageEditorAction) => void>();
|
|
46
|
-
render(
|
|
47
|
-
<LayersPanel
|
|
48
|
-
doc={buildDoc()}
|
|
49
|
-
selectedLayerId={null}
|
|
50
|
-
dispatch={dispatch}
|
|
51
|
-
onAddLayer={ignoreAddLayer}
|
|
52
|
-
/>,
|
|
53
|
-
);
|
|
54
|
-
const items = screen.getAllByText(/Bottom|Middle|Top/);
|
|
55
|
-
expect(items[0]?.textContent).toContain('Top');
|
|
56
|
-
expect(items[items.length - 1]?.textContent).toContain('Bottom');
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('clicking the layer name dispatches a select action', () => {
|
|
60
|
-
const dispatch = vi.fn<(a: ImageEditorAction) => void>();
|
|
61
|
-
render(
|
|
62
|
-
<LayersPanel
|
|
63
|
-
doc={buildDoc()}
|
|
64
|
-
selectedLayerId={null}
|
|
65
|
-
dispatch={dispatch}
|
|
66
|
-
onAddLayer={ignoreAddLayer}
|
|
67
|
-
/>,
|
|
68
|
-
);
|
|
69
|
-
fireEvent.click(screen.getByText('Middle'));
|
|
70
|
-
expect(dispatch).toHaveBeenCalledWith({ type: 'select', layerId: 'b' });
|
|
71
|
-
});
|
|
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
|
-
|
|
105
|
-
it('toggling visibility dispatches an update-layer action', () => {
|
|
106
|
-
const dispatch = vi.fn<(a: ImageEditorAction) => void>();
|
|
107
|
-
render(
|
|
108
|
-
<LayersPanel
|
|
109
|
-
doc={buildDoc()}
|
|
110
|
-
selectedLayerId={null}
|
|
111
|
-
dispatch={dispatch}
|
|
112
|
-
onAddLayer={ignoreAddLayer}
|
|
113
|
-
/>,
|
|
114
|
-
);
|
|
115
|
-
// The "Hide layer" buttons exist for visible layers.
|
|
116
|
-
const hideButtons = screen.getAllByRole('button', { name: 'Hide layer' });
|
|
117
|
-
fireEvent.click(hideButtons[0]!); // top-most visible layer is 'b'
|
|
118
|
-
expect(dispatch).toHaveBeenCalledWith({
|
|
119
|
-
type: 'update-layer',
|
|
120
|
-
layerId: 'b',
|
|
121
|
-
patch: { visible: false },
|
|
122
|
-
});
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it('move-up button is disabled at the top of the stack', () => {
|
|
126
|
-
const dispatch = vi.fn<(a: ImageEditorAction) => void>();
|
|
127
|
-
render(
|
|
128
|
-
<LayersPanel
|
|
129
|
-
doc={buildDoc()}
|
|
130
|
-
selectedLayerId={null}
|
|
131
|
-
dispatch={dispatch}
|
|
132
|
-
onAddLayer={ignoreAddLayer}
|
|
133
|
-
/>,
|
|
134
|
-
);
|
|
135
|
-
const upButtons = screen.getAllByRole('button', { name: 'Move layer up' });
|
|
136
|
-
// Visual order is c, b, a; layer 'c' is at the top of the stack and cannot move up.
|
|
137
|
-
expect((upButtons[0] as HTMLButtonElement).disabled).toBe(true);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it('delete button dispatches a remove-layer action', () => {
|
|
141
|
-
const dispatch = vi.fn<(a: ImageEditorAction) => void>();
|
|
142
|
-
render(
|
|
143
|
-
<LayersPanel
|
|
144
|
-
doc={buildDoc()}
|
|
145
|
-
selectedLayerId={null}
|
|
146
|
-
dispatch={dispatch}
|
|
147
|
-
onAddLayer={ignoreAddLayer}
|
|
148
|
-
/>,
|
|
149
|
-
);
|
|
150
|
-
const delButtons = screen.getAllByRole('button', { name: 'Delete layer' });
|
|
151
|
-
fireEvent.click(delButtons[0]!); // top of stack ('c')
|
|
152
|
-
expect(dispatch).toHaveBeenCalledWith({ type: 'remove-layer', layerId: 'c' });
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it('shows an empty-state message when there are no layers', () => {
|
|
156
|
-
const dispatch = vi.fn<(a: ImageEditorAction) => void>();
|
|
157
|
-
render(
|
|
158
|
-
<LayersPanel
|
|
159
|
-
doc={{ version: 1, canvas: { width: 1, height: 1 }, layers: [] }}
|
|
160
|
-
selectedLayerId={null}
|
|
161
|
-
dispatch={dispatch}
|
|
162
|
-
onAddLayer={ignoreAddLayer}
|
|
163
|
-
/>,
|
|
164
|
-
);
|
|
165
|
-
expect(screen.getByText(/no layers yet/i)).toBeTruthy();
|
|
166
|
-
});
|
|
167
|
-
});
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Layout child sub-blocks round-trip through the markdown ↔ Tiptap bridge.
|
|
3
|
-
*
|
|
4
|
-
* Layouts now persist each layer as a readable `### {#id} {[type …]}` child
|
|
5
|
-
* heading (no base64 `layers=` blob); a text layer's content is the child's
|
|
6
|
-
* markdown body. These tests pin that serialization seam — the same
|
|
7
|
-
* `data-template` / `data-template-params` / `data-block-attrs` attributes the
|
|
8
|
-
* LayoutAdapter reads back — without standing up a full editor.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { describe, it, expect } from 'vitest';
|
|
12
|
-
import { markdownToTiptap, tiptapToMarkdown } from '../tiptapBridge';
|
|
13
|
-
|
|
14
|
-
// Parent title "Pricing" (not "Layout") avoids the bridge's template-label
|
|
15
|
-
// de-dup heuristic, which would strip a title equal to the template's label.
|
|
16
|
-
const LAYOUT = [
|
|
17
|
-
'## Pricing {[layout]}',
|
|
18
|
-
'',
|
|
19
|
-
'### {#text-1} {[text x=360 y=380 width=1200 height=320 align=center]}',
|
|
20
|
-
'',
|
|
21
|
-
'Welcome to the **bold** layout',
|
|
22
|
-
'',
|
|
23
|
-
'- first point',
|
|
24
|
-
'- second point',
|
|
25
|
-
'',
|
|
26
|
-
'### {#box-1} {[rectangle x=760 y=390 width=400 height=300 fill="#e0e7ff"]}',
|
|
27
|
-
'',
|
|
28
|
-
'### {#pic-1} {[image x=40 y=40 width=320 height=240 src=cover.png alt="Cover"]}',
|
|
29
|
-
].join('\n');
|
|
30
|
-
|
|
31
|
-
describe('Layout child sub-block round-trip', () => {
|
|
32
|
-
it('extracts a text layer annotation into the template + block-attr attributes', () => {
|
|
33
|
-
const html = markdownToTiptap('### {#text-1} {[text x=360 y=380 width=1200 height=320]}');
|
|
34
|
-
expect(html).toContain('data-template="text"');
|
|
35
|
-
expect(html).toContain('data-template-params="x=360 y=380 width=1200 height=320"');
|
|
36
|
-
expect(html).toContain('data-block-attrs="#text-1"');
|
|
37
|
-
expect(html).not.toContain('{[text'); // raw annotation text stripped
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('keeps a text layer body (marks + list) as sibling blocks, not in the heading', () => {
|
|
41
|
-
const html = markdownToTiptap(
|
|
42
|
-
['### {#text-1} {[text x=0 y=0]}', '', 'Hello **world**', '', '- a', '- b'].join('\n'),
|
|
43
|
-
);
|
|
44
|
-
// Heading is empty; the body is its own paragraph + list.
|
|
45
|
-
expect(html).toContain('<strong>world</strong>');
|
|
46
|
-
expect(html).toContain('<ul>');
|
|
47
|
-
expect(html).toContain('<li>');
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('round-trips a full layout (text + box + image) losslessly and idempotently', () => {
|
|
51
|
-
const back = tiptapToMarkdown(markdownToTiptap(LAYOUT));
|
|
52
|
-
expect(back).toContain('## Pricing {[layout]}');
|
|
53
|
-
// Child sub-blocks keep their id + annotation (empty-text headings get an
|
|
54
|
-
// extra space after `###`, which re-parses identically — assert spacing-free).
|
|
55
|
-
expect(back).toContain('{#text-1} {[text x=360 y=380 width=1200 height=320 align=center]}');
|
|
56
|
-
expect(back).toContain(
|
|
57
|
-
'{#box-1} {[rectangle x=760 y=390 width=400 height=300 fill="#e0e7ff"]}',
|
|
58
|
-
);
|
|
59
|
-
expect(back).toContain(
|
|
60
|
-
'{#pic-1} {[image x=40 y=40 width=320 height=240 src=cover.png alt="Cover"]}',
|
|
61
|
-
);
|
|
62
|
-
// The text layer's body markdown survives as readable content.
|
|
63
|
-
expect(back).toContain('Welcome to the **bold** layout');
|
|
64
|
-
expect(back).toContain('- first point');
|
|
65
|
-
expect(back).toContain('- second point');
|
|
66
|
-
// No base64 layers blob anywhere — the whole point of the change.
|
|
67
|
-
expect(back).not.toContain('layers=');
|
|
68
|
-
// Stable: a second pass reproduces the first exactly.
|
|
69
|
-
expect(tiptapToMarkdown(markdownToTiptap(back))).toBe(back);
|
|
70
|
-
});
|
|
71
|
-
});
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Covers the LinkDialog's "Browse documents" picker added behind the
|
|
3
|
-
* `documentLinkProvider` prop. The picker is purely additive — when
|
|
4
|
-
* the prop is absent the dialog renders its original URL-only layout.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
8
|
-
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
9
|
-
import { LinkDialog } from '../LinkDialog';
|
|
10
|
-
import type { DocumentLinkProvider } from '../EditorContext';
|
|
11
|
-
|
|
12
|
-
const NEIGHBORS = [
|
|
13
|
-
{ path: 'resume.md', label: 'Resume', description: 'My CV' },
|
|
14
|
-
{ path: 'projects.md', label: 'Projects' },
|
|
15
|
-
{ path: 'misc/notes.md', label: 'Notes', description: 'Scratchpad' },
|
|
16
|
-
];
|
|
17
|
-
|
|
18
|
-
const provider: DocumentLinkProvider = async (q) => {
|
|
19
|
-
const query = q.trim().toLowerCase();
|
|
20
|
-
if (!query) return NEIGHBORS;
|
|
21
|
-
return NEIGHBORS.filter(
|
|
22
|
-
(n) => n.label.toLowerCase().includes(query) || n.path.toLowerCase().includes(query),
|
|
23
|
-
);
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
describe('LinkDialog — document picker', () => {
|
|
27
|
-
it('hides the documents tab when no provider is supplied', () => {
|
|
28
|
-
render(
|
|
29
|
-
<LinkDialog
|
|
30
|
-
mode="insert"
|
|
31
|
-
initialText=""
|
|
32
|
-
initialUrl=""
|
|
33
|
-
onConfirm={() => {}}
|
|
34
|
-
onClose={() => {}}
|
|
35
|
-
/>,
|
|
36
|
-
);
|
|
37
|
-
expect(screen.queryByRole('tab', { name: /browse documents/i })).toBeNull();
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('shows both tabs when the provider is supplied', () => {
|
|
41
|
-
render(
|
|
42
|
-
<LinkDialog
|
|
43
|
-
mode="insert"
|
|
44
|
-
initialText=""
|
|
45
|
-
initialUrl=""
|
|
46
|
-
onConfirm={() => {}}
|
|
47
|
-
onClose={() => {}}
|
|
48
|
-
documentLinkProvider={provider}
|
|
49
|
-
/>,
|
|
50
|
-
);
|
|
51
|
-
expect(screen.getByRole('tab', { name: 'URL' })).toBeTruthy();
|
|
52
|
-
expect(screen.getByRole('tab', { name: 'Browse documents' })).toBeTruthy();
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('lists initial candidates when the documents tab opens', async () => {
|
|
56
|
-
render(
|
|
57
|
-
<LinkDialog
|
|
58
|
-
mode="insert"
|
|
59
|
-
initialText=""
|
|
60
|
-
initialUrl=""
|
|
61
|
-
onConfirm={() => {}}
|
|
62
|
-
onClose={() => {}}
|
|
63
|
-
documentLinkProvider={provider}
|
|
64
|
-
/>,
|
|
65
|
-
);
|
|
66
|
-
fireEvent.click(screen.getByRole('tab', { name: 'Browse documents' }));
|
|
67
|
-
await waitFor(() => {
|
|
68
|
-
expect(screen.getByText('Resume')).toBeTruthy();
|
|
69
|
-
expect(screen.getByText('Projects')).toBeTruthy();
|
|
70
|
-
expect(screen.getByText('Notes')).toBeTruthy();
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('filters candidates as the user types', async () => {
|
|
75
|
-
render(
|
|
76
|
-
<LinkDialog
|
|
77
|
-
mode="insert"
|
|
78
|
-
initialText=""
|
|
79
|
-
initialUrl=""
|
|
80
|
-
onConfirm={() => {}}
|
|
81
|
-
onClose={() => {}}
|
|
82
|
-
documentLinkProvider={provider}
|
|
83
|
-
/>,
|
|
84
|
-
);
|
|
85
|
-
fireEvent.click(screen.getByRole('tab', { name: 'Browse documents' }));
|
|
86
|
-
await waitFor(() => screen.getByText('Resume'));
|
|
87
|
-
const search = screen.getByLabelText('Search') as HTMLInputElement;
|
|
88
|
-
fireEvent.change(search, { target: { value: 'proj' } });
|
|
89
|
-
await waitFor(() => {
|
|
90
|
-
expect(screen.getByText('Projects')).toBeTruthy();
|
|
91
|
-
expect(screen.queryByText('Resume')).toBeNull();
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('picking a document fills the URL and auto-fills the caption when empty', async () => {
|
|
96
|
-
const onConfirm = vi.fn();
|
|
97
|
-
render(
|
|
98
|
-
<LinkDialog
|
|
99
|
-
mode="insert"
|
|
100
|
-
initialText=""
|
|
101
|
-
initialUrl=""
|
|
102
|
-
onConfirm={onConfirm}
|
|
103
|
-
onClose={() => {}}
|
|
104
|
-
documentLinkProvider={provider}
|
|
105
|
-
/>,
|
|
106
|
-
);
|
|
107
|
-
fireEvent.click(screen.getByRole('tab', { name: 'Browse documents' }));
|
|
108
|
-
await waitFor(() => screen.getByText('Resume'));
|
|
109
|
-
fireEvent.click(screen.getByRole('option', { name: /Resume/i }));
|
|
110
|
-
// After picking, the dialog jumps back to the URL tab so Enter submits.
|
|
111
|
-
const urlInput = screen.getByLabelText('URL') as HTMLInputElement;
|
|
112
|
-
expect(urlInput.value).toBe('resume.md');
|
|
113
|
-
const textInput = screen.getByLabelText('Text') as HTMLInputElement;
|
|
114
|
-
expect(textInput.value).toBe('Resume');
|
|
115
|
-
// Submit
|
|
116
|
-
fireEvent.submit(urlInput.closest('form')!);
|
|
117
|
-
expect(onConfirm).toHaveBeenCalledWith('Resume', 'resume.md');
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it('preserves a caption the user already typed when picking a document', async () => {
|
|
121
|
-
render(
|
|
122
|
-
<LinkDialog
|
|
123
|
-
mode="insert"
|
|
124
|
-
initialText="Read my work"
|
|
125
|
-
initialUrl=""
|
|
126
|
-
onConfirm={() => {}}
|
|
127
|
-
onClose={() => {}}
|
|
128
|
-
documentLinkProvider={provider}
|
|
129
|
-
/>,
|
|
130
|
-
);
|
|
131
|
-
fireEvent.click(screen.getByRole('tab', { name: 'Browse documents' }));
|
|
132
|
-
await waitFor(() => screen.getByText('Resume'));
|
|
133
|
-
fireEvent.click(screen.getByRole('option', { name: /Resume/i }));
|
|
134
|
-
const textInput = screen.getByLabelText('Text') as HTMLInputElement;
|
|
135
|
-
expect(textInput.value).toBe('Read my work');
|
|
136
|
-
});
|
|
137
|
-
});
|
|
@@ -1,26 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import type { MediaProvider } from '@bendyline/squisq/schemas';
|
|
3
|
-
import { markdownToTiptap, tiptapToMarkdown } from '../tiptapBridge';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Attachment-flow regression: earlier versions of MediaBin dropped
|
|
7
|
-
* uploaded files into the bin without inserting a markdown ref into
|
|
8
|
-
* the editor body. A user would upload an image, hit Send in the
|
|
9
|
-
* downstream chat composer, and the outgoing markdown would have no
|
|
10
|
-
* image reference — the downstream consumer would reply "nothing came through."
|
|
11
|
-
*
|
|
12
|
-
* The fix: after `mediaProvider.addMedia(...)` succeeds, MediaBin
|
|
13
|
-
* fires `onMediaUploaded(relativePath, name, mimeType)`. The
|
|
14
|
-
* EditorShell wires this to an `insertAtCursor` that emits
|
|
15
|
-
* `` so the file actually participates
|
|
16
|
-
* in the outgoing markdown.
|
|
17
|
-
*
|
|
18
|
-
* These tests exercise the contract directly: the markdown snippet
|
|
19
|
-
* produced by the upload callback, once round-tripped through the
|
|
20
|
-
* editor's markdown↔HTML bridge, must round-trip back to a form
|
|
21
|
-
* the downstream service's image-extraction regex can see.
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
function fakeMediaProvider(records: string[]): MediaProvider {
|
|
25
|
-
let counter = 0;
|
|
26
|
-
return {
|
|
27
|
-
async addMedia(name: string, _data: ArrayBuffer | Blob | Uint8Array, _mime: string) {
|
|
28
|
-
counter += 1;
|
|
29
|
-
const relative = `attachments/${counter}-${name}`;
|
|
30
|
-
records.push(relative);
|
|
31
|
-
return relative;
|
|
32
|
-
},
|
|
33
|
-
async resolveUrl(relPath: string) {
|
|
34
|
-
return relPath;
|
|
35
|
-
},
|
|
36
|
-
async listMedia() {
|
|
37
|
-
return [];
|
|
38
|
-
},
|
|
39
|
-
async removeMedia(_relPath: string) {
|
|
40
|
-
/* no-op */
|
|
41
|
-
},
|
|
42
|
-
dispose() {
|
|
43
|
-
/* no-op */
|
|
44
|
-
},
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Re-implements the exact snippet EditorShell's `insertMediaRef`
|
|
50
|
-
* builds. Keeping this aligned with the real impl would ordinarily
|
|
51
|
-
* rely on directly importing the helper; since it's currently inline
|
|
52
|
-
* in EditorShell, mirror the logic here and lean on the test to
|
|
53
|
-
* alert us if we drift apart.
|
|
54
|
-
*/
|
|
55
|
-
function buildAttachmentSnippet(relativePath: string, name: string, mimeType: string): string {
|
|
56
|
-
const alt = name.replace(/\.[^.]+$/, '').replace(/[-_]/g, ' ');
|
|
57
|
-
return mimeType.startsWith('image/') ? `` : `[${alt}](${relativePath})`;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
describe('media attachment flow', () => {
|
|
61
|
-
it('addMedia → buildAttachmentSnippet → markdown round-trip keeps the ref', async () => {
|
|
62
|
-
const records: string[] = [];
|
|
63
|
-
const provider = fakeMediaProvider(records);
|
|
64
|
-
|
|
65
|
-
// Simulate MediaBin.handleFileChange for a single PNG drop.
|
|
66
|
-
const pngBytes = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
|
|
67
|
-
const relative = await provider.addMedia('my_screenshot.png', pngBytes, 'image/png');
|
|
68
|
-
|
|
69
|
-
expect(records).toEqual([relative]);
|
|
70
|
-
const snippet = buildAttachmentSnippet(relative, 'my_screenshot.png', 'image/png');
|
|
71
|
-
expect(snippet).toBe(``);
|
|
72
|
-
|
|
73
|
-
// Insert snippet into the editor: markdown → HTML → markdown.
|
|
74
|
-
// This is the path a real insertAtCursor + tiptap onUpdate goes
|
|
75
|
-
// through. The outbound markdown must still contain the ref.
|
|
76
|
-
const html = markdownToTiptap(snippet);
|
|
77
|
-
expect(html).toMatch(/<img\b/);
|
|
78
|
-
expect(html).toContain(`src="${relative}"`);
|
|
79
|
-
|
|
80
|
-
const back = tiptapToMarkdown(html);
|
|
81
|
-
expect(back).toContain(``);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('handles empty-alt (most common pasted-image shape)', async () => {
|
|
85
|
-
const records: string[] = [];
|
|
86
|
-
const provider = fakeMediaProvider(records);
|
|
87
|
-
const relative = await provider.addMedia('pasted.png', new Uint8Array([0]), 'image/png');
|
|
88
|
-
|
|
89
|
-
// Simulate what happens when alt is empty — common for bare pastes
|
|
90
|
-
// where the user hasn't typed a caption.
|
|
91
|
-
const snippet = ``;
|
|
92
|
-
const html = markdownToTiptap(snippet);
|
|
93
|
-
expect(html).toMatch(/<img\b/);
|
|
94
|
-
expect(html).toContain(`src="${relative}"`);
|
|
95
|
-
const back = tiptapToMarkdown(html);
|
|
96
|
-
expect(back).toContain(``);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('non-image files fall back to a plain link, still preserving the ref', async () => {
|
|
100
|
-
const records: string[] = [];
|
|
101
|
-
const provider = fakeMediaProvider(records);
|
|
102
|
-
const relative = await provider.addMedia('design.pdf', new Uint8Array([0]), 'application/pdf');
|
|
103
|
-
const snippet = buildAttachmentSnippet(relative, 'design.pdf', 'application/pdf');
|
|
104
|
-
expect(snippet).toBe(`[design](${relative})`);
|
|
105
|
-
// Non-images don't go through the `<img>` regex — they stay as
|
|
106
|
-
// plain markdown links, which the service-side extractor ignores
|
|
107
|
-
// but the UI renders as normal hyperlinks.
|
|
108
|
-
expect(snippet).not.toContain('!');
|
|
109
|
-
});
|
|
110
|
-
});
|
|
@@ -1,90 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
collectMediaReferencesFromMarkdown,
|
|
4
|
-
removeMediaReferencesFromMarkdown,
|
|
5
|
-
} from '../mediaReferences';
|
|
6
|
-
|
|
7
|
-
describe('collectMediaReferencesFromMarkdown', () => {
|
|
8
|
-
it('collects markdown image/link and html media references', () => {
|
|
9
|
-
const refs = collectMediaReferencesFromMarkdown(
|
|
10
|
-
[
|
|
11
|
-
'',
|
|
12
|
-
'[Two](attachments/two.pdf)',
|
|
13
|
-
'<video src="video/clip.webm" poster="video/poster.png"></video>',
|
|
14
|
-
'<a href="attachments/three.txt">Three</a>',
|
|
15
|
-
].join('\n'),
|
|
16
|
-
);
|
|
17
|
-
|
|
18
|
-
expect([...refs].sort()).toEqual([
|
|
19
|
-
'attachments/one.png',
|
|
20
|
-
'attachments/three.txt',
|
|
21
|
-
'attachments/two.pdf',
|
|
22
|
-
'video/clip.webm',
|
|
23
|
-
'video/poster.png',
|
|
24
|
-
]);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('collects media references from squiggly annotations', () => {
|
|
28
|
-
const refs = collectMediaReferencesFromMarkdown(
|
|
29
|
-
[
|
|
30
|
-
'## Intro {[audio=audio/take.mp3]}',
|
|
31
|
-
'{[video src="video/clip with spaces.webm"]}',
|
|
32
|
-
'### Image {#hero} {[image src=images/hero.png alt="Hero"]}',
|
|
33
|
-
].join('\n'),
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
expect([...refs].sort()).toEqual([
|
|
37
|
-
'audio/take.mp3',
|
|
38
|
-
'images/hero.png',
|
|
39
|
-
'video/clip with spaces.webm',
|
|
40
|
-
]);
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
describe('removeMediaReferencesFromMarkdown', () => {
|
|
45
|
-
it('removes standalone image references to the media path', () => {
|
|
46
|
-
const source = ['Before', '', '', '', 'After'].join('\n');
|
|
47
|
-
|
|
48
|
-
expect(removeMediaReferencesFromMarkdown(source, 'attachments/pasted.png')).toBe(
|
|
49
|
-
['Before', '', '', 'After'].join('\n'),
|
|
50
|
-
);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('removes inline image and link references without touching similar paths', () => {
|
|
54
|
-
const source = [
|
|
55
|
-
'Keep .',
|
|
56
|
-
'Drop  and [file](attachments/pasted.png).',
|
|
57
|
-
].join('\n');
|
|
58
|
-
|
|
59
|
-
expect(removeMediaReferencesFromMarkdown(source, 'attachments/pasted.png')).toBe(
|
|
60
|
-
['Keep .', 'Drop and .'].join('\n'),
|
|
61
|
-
);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('supports angle-wrapped destinations and markdown titles', () => {
|
|
65
|
-
const source =
|
|
66
|
-
'Drop  and [A](attachments/simple.png "title").';
|
|
67
|
-
|
|
68
|
-
expect(removeMediaReferencesFromMarkdown(source, 'attachments/my pasted image.png')).toBe(
|
|
69
|
-
'Drop and [A](attachments/simple.png "title").',
|
|
70
|
-
);
|
|
71
|
-
expect(removeMediaReferencesFromMarkdown(source, 'attachments/simple.png')).toBe(
|
|
72
|
-
'Drop  and .',
|
|
73
|
-
);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('removes resized raw HTML image tags and raw HTML anchors', () => {
|
|
77
|
-
const source =
|
|
78
|
-
'<img alt="Screenshot" src="attachments/pasted.png" width="320">\n<a href="attachments/pasted.png">Screenshot</a>';
|
|
79
|
-
|
|
80
|
-
expect(removeMediaReferencesFromMarkdown(source, 'attachments/pasted.png')).toBe('\n');
|
|
81
|
-
});
|
|
82
|
-
});
|