@bendyline/squisq-editor-react 1.4.0 → 1.5.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/dist/DocumentSettingsDialog.d.ts +26 -0
- package/dist/DocumentSettingsDialog.d.ts.map +1 -0
- package/dist/DocumentSettingsDialog.js +115 -0
- package/dist/DocumentSettingsDialog.js.map +1 -0
- package/dist/EditorContext.d.ts +248 -4
- package/dist/EditorContext.d.ts.map +1 -1
- package/dist/EditorContext.js +248 -10
- package/dist/EditorContext.js.map +1 -1
- package/dist/EditorShell.d.ts +173 -4
- package/dist/EditorShell.d.ts.map +1 -1
- package/dist/EditorShell.js +110 -10
- package/dist/EditorShell.js.map +1 -1
- package/dist/EmojiPicker.d.ts +50 -0
- package/dist/EmojiPicker.d.ts.map +1 -0
- package/dist/EmojiPicker.js +182 -0
- package/dist/EmojiPicker.js.map +1 -0
- package/dist/ImageEditor.d.ts +68 -0
- package/dist/ImageEditor.d.ts.map +1 -0
- package/dist/ImageEditor.js +166 -0
- package/dist/ImageEditor.js.map +1 -0
- package/dist/ImageNodeView.d.ts +13 -1
- package/dist/ImageNodeView.d.ts.map +1 -1
- package/dist/ImageNodeView.js +172 -19
- package/dist/ImageNodeView.js.map +1 -1
- package/dist/ImageViewer.d.ts +26 -0
- package/dist/ImageViewer.d.ts.map +1 -0
- package/dist/ImageViewer.js +119 -0
- package/dist/ImageViewer.js.map +1 -0
- package/dist/InlineIcon.d.ts +17 -0
- package/dist/InlineIcon.d.ts.map +1 -0
- package/dist/InlineIcon.js +72 -0
- package/dist/InlineIcon.js.map +1 -0
- package/dist/InlinePreviewGutter.d.ts +52 -0
- package/dist/InlinePreviewGutter.d.ts.map +1 -0
- package/dist/InlinePreviewGutter.js +397 -0
- package/dist/InlinePreviewGutter.js.map +1 -0
- package/dist/LinkDialog.d.ts +43 -0
- package/dist/LinkDialog.d.ts.map +1 -0
- package/dist/LinkDialog.js +102 -0
- package/dist/LinkDialog.js.map +1 -0
- package/dist/MentionExtension.js +10 -7
- package/dist/MentionExtension.js.map +1 -1
- package/dist/OutlinePanel.d.ts +17 -0
- package/dist/OutlinePanel.d.ts.map +1 -0
- package/dist/OutlinePanel.js +167 -0
- package/dist/OutlinePanel.js.map +1 -0
- package/dist/PlainHtmlPreview.d.ts +50 -0
- package/dist/PlainHtmlPreview.d.ts.map +1 -0
- package/dist/PlainHtmlPreview.js +155 -0
- package/dist/PlainHtmlPreview.js.map +1 -0
- package/dist/PreviewControls.d.ts +15 -1
- package/dist/PreviewControls.d.ts.map +1 -1
- package/dist/PreviewControls.js +75 -18
- package/dist/PreviewControls.js.map +1 -1
- package/dist/PreviewPanel.d.ts +11 -10
- package/dist/PreviewPanel.d.ts.map +1 -1
- package/dist/PreviewPanel.js +20 -17
- package/dist/PreviewPanel.js.map +1 -1
- package/dist/RawEditor.d.ts.map +1 -1
- package/dist/RawEditor.js +198 -4
- package/dist/RawEditor.js.map +1 -1
- package/dist/RecorderEntry.d.ts +24 -0
- package/dist/RecorderEntry.d.ts.map +1 -0
- package/dist/RecorderEntry.js +139 -0
- package/dist/RecorderEntry.js.map +1 -0
- package/dist/TemplateAnnotation.d.ts.map +1 -1
- package/dist/TemplateAnnotation.js +32 -6
- package/dist/TemplateAnnotation.js.map +1 -1
- package/dist/TemplatePicker.d.ts +53 -0
- package/dist/TemplatePicker.d.ts.map +1 -0
- package/dist/TemplatePicker.js +388 -0
- package/dist/TemplatePicker.js.map +1 -0
- package/dist/ThemeCustomizerPanel.d.ts +32 -0
- package/dist/ThemeCustomizerPanel.d.ts.map +1 -0
- package/dist/ThemeCustomizerPanel.js +256 -0
- package/dist/ThemeCustomizerPanel.js.map +1 -0
- package/dist/ThemePicker.d.ts +33 -0
- package/dist/ThemePicker.d.ts.map +1 -0
- package/dist/ThemePicker.js +148 -0
- package/dist/ThemePicker.js.map +1 -0
- package/dist/Toolbar.d.ts.map +1 -1
- package/dist/Toolbar.js +508 -33
- package/dist/Toolbar.js.map +1 -1
- package/dist/VersionHistoryPanel.d.ts +14 -0
- package/dist/VersionHistoryPanel.d.ts.map +1 -0
- package/dist/VersionHistoryPanel.js +147 -0
- package/dist/VersionHistoryPanel.js.map +1 -0
- package/dist/ViewMenuPanel.d.ts +13 -0
- package/dist/ViewMenuPanel.d.ts.map +1 -0
- package/dist/ViewMenuPanel.js +58 -0
- package/dist/ViewMenuPanel.js.map +1 -0
- package/dist/WysiwygEditor.d.ts.map +1 -1
- package/dist/WysiwygEditor.js +198 -9
- package/dist/WysiwygEditor.js.map +1 -1
- package/dist/__tests__/detectMarkdown.test.js +0 -14
- package/dist/__tests__/detectMarkdown.test.js.map +1 -1
- package/dist/__tests__/documentSettingsDialog.test.d.ts +2 -0
- package/dist/__tests__/documentSettingsDialog.test.d.ts.map +1 -0
- package/dist/__tests__/documentSettingsDialog.test.js +132 -0
- package/dist/__tests__/documentSettingsDialog.test.js.map +1 -0
- package/dist/__tests__/emojiPicker.test.d.ts +2 -0
- package/dist/__tests__/emojiPicker.test.d.ts.map +1 -0
- package/dist/__tests__/emojiPicker.test.js +111 -0
- package/dist/__tests__/emojiPicker.test.js.map +1 -0
- package/dist/__tests__/fileKind.test.js +13 -0
- package/dist/__tests__/fileKind.test.js.map +1 -1
- package/dist/__tests__/imageEditAffordance.test.d.ts +2 -0
- package/dist/__tests__/imageEditAffordance.test.d.ts.map +1 -0
- package/dist/__tests__/imageEditAffordance.test.js +188 -0
- package/dist/__tests__/imageEditAffordance.test.js.map +1 -0
- package/dist/__tests__/imageEditorShell.test.d.ts +2 -0
- package/dist/__tests__/imageEditorShell.test.d.ts.map +1 -0
- package/dist/__tests__/imageEditorShell.test.js +52 -0
- package/dist/__tests__/imageEditorShell.test.js.map +1 -0
- package/dist/__tests__/imageEditorState.test.d.ts +3 -0
- package/dist/__tests__/imageEditorState.test.d.ts.map +1 -0
- package/dist/__tests__/imageEditorState.test.js +148 -0
- package/dist/__tests__/imageEditorState.test.js.map +1 -0
- package/dist/__tests__/inlinePreviewGutter.test.d.ts +2 -0
- package/dist/__tests__/inlinePreviewGutter.test.d.ts.map +1 -0
- package/dist/__tests__/inlinePreviewGutter.test.js +51 -0
- package/dist/__tests__/inlinePreviewGutter.test.js.map +1 -0
- package/dist/__tests__/inlinePreviewGutterAllBlocks.test.d.ts +2 -0
- package/dist/__tests__/inlinePreviewGutterAllBlocks.test.d.ts.map +1 -0
- package/dist/__tests__/inlinePreviewGutterAllBlocks.test.js +63 -0
- package/dist/__tests__/inlinePreviewGutterAllBlocks.test.js.map +1 -0
- package/dist/__tests__/jsonEditor.test.d.ts +2 -0
- package/dist/__tests__/jsonEditor.test.d.ts.map +1 -0
- package/dist/__tests__/jsonEditor.test.js +134 -0
- package/dist/__tests__/jsonEditor.test.js.map +1 -0
- package/dist/__tests__/layersPanel.test.d.ts +2 -0
- package/dist/__tests__/layersPanel.test.d.ts.map +1 -0
- package/dist/__tests__/layersPanel.test.js +84 -0
- package/dist/__tests__/layersPanel.test.js.map +1 -0
- package/dist/__tests__/linkDialogDocPicker.test.d.ts +7 -0
- package/dist/__tests__/linkDialogDocPicker.test.d.ts.map +1 -0
- package/dist/__tests__/linkDialogDocPicker.test.js +75 -0
- package/dist/__tests__/linkDialogDocPicker.test.js.map +1 -0
- package/dist/__tests__/outlinePanel.test.d.ts +2 -0
- package/dist/__tests__/outlinePanel.test.d.ts.map +1 -0
- package/dist/__tests__/outlinePanel.test.js +68 -0
- package/dist/__tests__/outlinePanel.test.js.map +1 -0
- package/dist/__tests__/plainHtmlPreview.test.d.ts +2 -0
- package/dist/__tests__/plainHtmlPreview.test.d.ts.map +1 -0
- package/dist/__tests__/plainHtmlPreview.test.js +87 -0
- package/dist/__tests__/plainHtmlPreview.test.js.map +1 -0
- package/dist/__tests__/propertiesPanel.test.d.ts +2 -0
- package/dist/__tests__/propertiesPanel.test.d.ts.map +1 -0
- package/dist/__tests__/propertiesPanel.test.js +64 -0
- package/dist/__tests__/propertiesPanel.test.js.map +1 -0
- package/dist/__tests__/recorderFormats.test.d.ts +2 -0
- package/dist/__tests__/recorderFormats.test.d.ts.map +1 -0
- package/dist/__tests__/recorderFormats.test.js +121 -0
- package/dist/__tests__/recorderFormats.test.js.map +1 -0
- package/dist/__tests__/recorderTimingJson.test.d.ts +2 -0
- package/dist/__tests__/recorderTimingJson.test.d.ts.map +1 -0
- package/dist/__tests__/recorderTimingJson.test.js +37 -0
- package/dist/__tests__/recorderTimingJson.test.js.map +1 -0
- package/dist/__tests__/templateAnnotationRoundTrip.test.d.ts +2 -0
- package/dist/__tests__/templateAnnotationRoundTrip.test.d.ts.map +1 -0
- package/dist/__tests__/templateAnnotationRoundTrip.test.js +31 -0
- package/dist/__tests__/templateAnnotationRoundTrip.test.js.map +1 -0
- package/dist/__tests__/tiptapBridge.test.js +13 -0
- package/dist/__tests__/tiptapBridge.test.js.map +1 -1
- package/dist/__tests__/useImageEditor.test.d.ts +2 -0
- package/dist/__tests__/useImageEditor.test.d.ts.map +1 -0
- package/dist/__tests__/useImageEditor.test.js +131 -0
- package/dist/__tests__/useImageEditor.test.js.map +1 -0
- package/dist/__tests__/useMediaRecorder.test.d.ts +2 -0
- package/dist/__tests__/useMediaRecorder.test.d.ts.map +1 -0
- package/dist/__tests__/useMediaRecorder.test.js +153 -0
- package/dist/__tests__/useMediaRecorder.test.js.map +1 -0
- package/dist/__tests__/versionHistory.test.d.ts +2 -0
- package/dist/__tests__/versionHistory.test.d.ts.map +1 -0
- package/dist/__tests__/versionHistory.test.js +124 -0
- package/dist/__tests__/versionHistory.test.js.map +1 -0
- package/dist/blockSlice.d.ts +24 -0
- package/dist/blockSlice.d.ts.map +1 -0
- package/dist/blockSlice.js +63 -0
- package/dist/blockSlice.js.map +1 -0
- package/dist/buildPreviewDoc.d.ts.map +1 -1
- package/dist/buildPreviewDoc.js +52 -2
- package/dist/buildPreviewDoc.js.map +1 -1
- package/dist/emojiData.d.ts +81 -0
- package/dist/emojiData.d.ts.map +1 -0
- package/dist/emojiData.js +1283 -0
- package/dist/emojiData.js.map +1 -0
- package/dist/fileKind.d.ts +6 -2
- package/dist/fileKind.d.ts.map +1 -1
- package/dist/fileKind.js +25 -4
- package/dist/fileKind.js.map +1 -1
- package/dist/hooks/useFileDrop.d.ts.map +1 -1
- package/dist/hooks/useFileDrop.js +40 -4
- package/dist/hooks/useFileDrop.js.map +1 -1
- package/dist/imageEditor/CanvasSurface.d.ts +31 -0
- package/dist/imageEditor/CanvasSurface.d.ts.map +1 -0
- package/dist/imageEditor/CanvasSurface.js +264 -0
- package/dist/imageEditor/CanvasSurface.js.map +1 -0
- package/dist/imageEditor/ImageVersionHistoryDropdown.d.ts +39 -0
- package/dist/imageEditor/ImageVersionHistoryDropdown.d.ts.map +1 -0
- package/dist/imageEditor/ImageVersionHistoryDropdown.js +283 -0
- package/dist/imageEditor/ImageVersionHistoryDropdown.js.map +1 -0
- package/dist/imageEditor/LayersPanel.d.ts +14 -0
- package/dist/imageEditor/LayersPanel.d.ts.map +1 -0
- package/dist/imageEditor/LayersPanel.js +43 -0
- package/dist/imageEditor/LayersPanel.js.map +1 -0
- package/dist/imageEditor/PropertiesPanel.d.ts +14 -0
- package/dist/imageEditor/PropertiesPanel.d.ts.map +1 -0
- package/dist/imageEditor/PropertiesPanel.js +97 -0
- package/dist/imageEditor/PropertiesPanel.js.map +1 -0
- package/dist/imageEditor/Toolbar.d.ts +30 -0
- package/dist/imageEditor/Toolbar.d.ts.map +1 -0
- package/dist/imageEditor/Toolbar.js +108 -0
- package/dist/imageEditor/Toolbar.js.map +1 -0
- package/dist/imageEditor/icons.d.ts +24 -0
- package/dist/imageEditor/icons.d.ts.map +1 -0
- package/dist/imageEditor/icons.js +45 -0
- package/dist/imageEditor/icons.js.map +1 -0
- package/dist/imageEditor/layers/EditorImageLayer.d.ts +16 -0
- package/dist/imageEditor/layers/EditorImageLayer.d.ts.map +1 -0
- package/dist/imageEditor/layers/EditorImageLayer.js +37 -0
- package/dist/imageEditor/layers/EditorImageLayer.js.map +1 -0
- package/dist/imageEditor/layers/EditorShapeLayer.d.ts +15 -0
- package/dist/imageEditor/layers/EditorShapeLayer.d.ts.map +1 -0
- package/dist/imageEditor/layers/EditorShapeLayer.js +20 -0
- package/dist/imageEditor/layers/EditorShapeLayer.js.map +1 -0
- package/dist/imageEditor/layers/EditorTextLayer.d.ts +18 -0
- package/dist/imageEditor/layers/EditorTextLayer.d.ts.map +1 -0
- package/dist/imageEditor/layers/EditorTextLayer.js +13 -0
- package/dist/imageEditor/layers/EditorTextLayer.js.map +1 -0
- package/dist/imageEditor/layers/SelectionHandles.d.ts +17 -0
- package/dist/imageEditor/layers/SelectionHandles.d.ts.map +1 -0
- package/dist/imageEditor/layers/SelectionHandles.js +19 -0
- package/dist/imageEditor/layers/SelectionHandles.js.map +1 -0
- package/dist/imageEditor/state.d.ts +76 -0
- package/dist/imageEditor/state.d.ts.map +1 -0
- package/dist/imageEditor/state.js +87 -0
- package/dist/imageEditor/state.js.map +1 -0
- package/dist/imageEditor/useImageEditor.d.ts +53 -0
- package/dist/imageEditor/useImageEditor.d.ts.map +1 -0
- package/dist/imageEditor/useImageEditor.js +244 -0
- package/dist/imageEditor/useImageEditor.js.map +1 -0
- package/dist/imageEditor/useImageEditorTokens.d.ts +16 -0
- package/dist/imageEditor/useImageEditorTokens.d.ts.map +1 -0
- package/dist/imageEditor/useImageEditorTokens.js +45 -0
- package/dist/imageEditor/useImageEditorTokens.js.map +1 -0
- package/dist/index.d.ts +48 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -1
- package/dist/jsonEditor/EmbeddedRichTextField.d.ts +15 -0
- package/dist/jsonEditor/EmbeddedRichTextField.d.ts.map +1 -0
- package/dist/jsonEditor/EmbeddedRichTextField.js +74 -0
- package/dist/jsonEditor/EmbeddedRichTextField.js.map +1 -0
- package/dist/jsonEditor/JsonEditor.d.ts +36 -0
- package/dist/jsonEditor/JsonEditor.d.ts.map +1 -0
- package/dist/jsonEditor/JsonEditor.js +15 -0
- package/dist/jsonEditor/JsonEditor.js.map +1 -0
- package/dist/jsonEditor/JsonEditorContext.d.ts +28 -0
- package/dist/jsonEditor/JsonEditorContext.d.ts.map +1 -0
- package/dist/jsonEditor/JsonEditorContext.js +41 -0
- package/dist/jsonEditor/JsonEditorContext.js.map +1 -0
- package/dist/jsonEditor/RenderNode.d.ts +16 -0
- package/dist/jsonEditor/RenderNode.d.ts.map +1 -0
- package/dist/jsonEditor/RenderNode.js +32 -0
- package/dist/jsonEditor/RenderNode.js.map +1 -0
- package/dist/jsonEditor/editors.d.ts +36 -0
- package/dist/jsonEditor/editors.d.ts.map +1 -0
- package/dist/jsonEditor/editors.js +347 -0
- package/dist/jsonEditor/editors.js.map +1 -0
- package/dist/jsonEditor/index.d.ts +3 -0
- package/dist/jsonEditor/index.d.ts.map +1 -0
- package/dist/jsonEditor/index.js +2 -0
- package/dist/jsonEditor/index.js.map +1 -0
- package/dist/jsonEditor/useJsonEditorTokens.d.ts +13 -0
- package/dist/jsonEditor/useJsonEditorTokens.d.ts.map +1 -0
- package/dist/jsonEditor/useJsonEditorTokens.js +38 -0
- package/dist/jsonEditor/useJsonEditorTokens.js.map +1 -0
- package/dist/recorder/RecorderButton.d.ts +31 -0
- package/dist/recorder/RecorderButton.d.ts.map +1 -0
- package/dist/recorder/RecorderButton.js +24 -0
- package/dist/recorder/RecorderButton.js.map +1 -0
- package/dist/recorder/RecorderModal.d.ts +59 -0
- package/dist/recorder/RecorderModal.d.ts.map +1 -0
- package/dist/recorder/RecorderModal.js +333 -0
- package/dist/recorder/RecorderModal.js.map +1 -0
- package/dist/recorder/RecorderPanel.d.ts +25 -0
- package/dist/recorder/RecorderPanel.d.ts.map +1 -0
- package/dist/recorder/RecorderPanel.js +30 -0
- package/dist/recorder/RecorderPanel.js.map +1 -0
- package/dist/recorder/formats.d.ts +51 -0
- package/dist/recorder/formats.d.ts.map +1 -0
- package/dist/recorder/formats.js +144 -0
- package/dist/recorder/formats.js.map +1 -0
- package/dist/recorder/hooks/useMediaRecorder.d.ts +90 -0
- package/dist/recorder/hooks/useMediaRecorder.d.ts.map +1 -0
- package/dist/recorder/hooks/useMediaRecorder.js +277 -0
- package/dist/recorder/hooks/useMediaRecorder.js.map +1 -0
- package/dist/recorder/hooks/useStreamPreview.d.ts +22 -0
- package/dist/recorder/hooks/useStreamPreview.d.ts.map +1 -0
- package/dist/recorder/hooks/useStreamPreview.js +44 -0
- package/dist/recorder/hooks/useStreamPreview.js.map +1 -0
- package/dist/recorder/sources/cameraStream.d.ts +22 -0
- package/dist/recorder/sources/cameraStream.d.ts.map +1 -0
- package/dist/recorder/sources/cameraStream.js +24 -0
- package/dist/recorder/sources/cameraStream.js.map +1 -0
- package/dist/recorder/sources/micStream.d.ts +15 -0
- package/dist/recorder/sources/micStream.d.ts.map +1 -0
- package/dist/recorder/sources/micStream.js +24 -0
- package/dist/recorder/sources/micStream.js.map +1 -0
- package/dist/recorder/sources/screenStream.d.ts +53 -0
- package/dist/recorder/sources/screenStream.d.ts.map +1 -0
- package/dist/recorder/sources/screenStream.js +114 -0
- package/dist/recorder/sources/screenStream.js.map +1 -0
- package/dist/recorder/timingJson.d.ts +51 -0
- package/dist/recorder/timingJson.d.ts.map +1 -0
- package/dist/recorder/timingJson.js +42 -0
- package/dist/recorder/timingJson.js.map +1 -0
- package/dist/tiptap/TiptapAudio.d.ts +26 -0
- package/dist/tiptap/TiptapAudio.d.ts.map +1 -0
- package/dist/tiptap/TiptapAudio.js +58 -0
- package/dist/tiptap/TiptapAudio.js.map +1 -0
- package/dist/tiptap/TiptapVideo.d.ts +30 -0
- package/dist/tiptap/TiptapVideo.d.ts.map +1 -0
- package/dist/tiptap/TiptapVideo.js +66 -0
- package/dist/tiptap/TiptapVideo.js.map +1 -0
- package/dist/tiptap/useResolvedMediaSrc.d.ts +2 -0
- package/dist/tiptap/useResolvedMediaSrc.d.ts.map +1 -0
- package/dist/tiptap/useResolvedMediaSrc.js +42 -0
- package/dist/tiptap/useResolvedMediaSrc.js.map +1 -0
- package/dist/tiptapBridge.d.ts.map +1 -1
- package/dist/tiptapBridge.js +171 -14
- package/dist/tiptapBridge.js.map +1 -1
- package/dist/useHeadingLayout.d.ts +54 -0
- package/dist/useHeadingLayout.d.ts.map +1 -0
- package/dist/useHeadingLayout.js +260 -0
- package/dist/useHeadingLayout.js.map +1 -0
- package/dist/utils/collectInlineFontAwesomeCss.d.ts +21 -0
- package/dist/utils/collectInlineFontAwesomeCss.d.ts.map +1 -0
- package/dist/utils/collectInlineFontAwesomeCss.js +68 -0
- package/dist/utils/collectInlineFontAwesomeCss.js.map +1 -0
- package/dist/utils/dropUtils.d.ts +21 -2
- package/dist/utils/dropUtils.d.ts.map +1 -1
- package/dist/utils/dropUtils.js +43 -4
- package/dist/utils/dropUtils.js.map +1 -1
- package/dist/utils/normalizeMalformedAssetUrl.d.ts +15 -0
- package/dist/utils/normalizeMalformedAssetUrl.d.ts.map +1 -0
- package/dist/utils/normalizeMalformedAssetUrl.js +27 -0
- package/dist/utils/normalizeMalformedAssetUrl.js.map +1 -0
- package/package.json +8 -5
- package/src/DocumentSettingsDialog.tsx +266 -0
- package/src/EditorContext.tsx +534 -10
- package/src/EditorShell.tsx +571 -55
- package/src/EmojiPicker.tsx +332 -0
- package/src/ImageEditor.tsx +327 -0
- package/src/ImageNodeView.tsx +222 -21
- package/src/ImageViewer.tsx +221 -0
- package/src/InlineIcon.ts +84 -0
- package/src/InlinePreviewGutter.tsx +582 -0
- package/src/LinkDialog.tsx +276 -0
- package/src/MentionExtension.tsx +10 -7
- package/src/OutlinePanel.tsx +295 -0
- package/src/PlainHtmlPreview.tsx +211 -0
- package/src/PreviewControls.tsx +130 -24
- package/src/PreviewPanel.tsx +38 -21
- package/src/RawEditor.tsx +215 -4
- package/src/RecorderEntry.tsx +164 -0
- package/src/TemplateAnnotation.ts +32 -6
- package/src/TemplatePicker.tsx +818 -0
- package/src/ThemeCustomizerPanel.tsx +595 -0
- package/src/ThemePicker.tsx +319 -0
- package/src/Toolbar.tsx +708 -111
- package/src/VersionHistoryPanel.tsx +329 -0
- package/src/ViewMenuPanel.tsx +188 -0
- package/src/WysiwygEditor.tsx +229 -9
- package/src/__tests__/detectMarkdown.test.ts +0 -15
- package/src/__tests__/documentSettingsDialog.test.tsx +147 -0
- package/src/__tests__/emojiPicker.test.tsx +133 -0
- package/src/__tests__/fileKind.test.ts +16 -0
- package/src/__tests__/imageEditAffordance.test.tsx +268 -0
- package/src/__tests__/imageEditorShell.test.tsx +57 -0
- package/src/__tests__/imageEditorState.test.ts +171 -0
- package/src/__tests__/inlinePreviewGutter.test.tsx +62 -0
- package/src/__tests__/inlinePreviewGutterAllBlocks.test.tsx +103 -0
- package/src/__tests__/jsonEditor.test.tsx +168 -0
- package/src/__tests__/layersPanel.test.tsx +97 -0
- package/src/__tests__/linkDialogDocPicker.test.tsx +137 -0
- package/src/__tests__/outlinePanel.test.tsx +79 -0
- package/src/__tests__/plainHtmlPreview.test.tsx +107 -0
- package/src/__tests__/propertiesPanel.test.tsx +69 -0
- package/src/__tests__/recorderFormats.test.ts +146 -0
- package/src/__tests__/recorderTimingJson.test.ts +41 -0
- package/src/__tests__/templateAnnotationRoundTrip.test.ts +34 -0
- package/src/__tests__/tiptapBridge.test.ts +15 -0
- package/src/__tests__/useImageEditor.test.tsx +159 -0
- package/src/__tests__/useMediaRecorder.test.ts +186 -0
- package/src/__tests__/versionHistory.test.tsx +197 -0
- package/src/blockSlice.ts +75 -0
- package/src/buildPreviewDoc.ts +61 -6
- package/src/emojiData.ts +1337 -0
- package/src/fileKind.ts +30 -6
- package/src/hooks/useFileDrop.ts +40 -4
- package/src/imageEditor/CanvasSurface.tsx +402 -0
- package/src/imageEditor/ImageVersionHistoryDropdown.tsx +396 -0
- package/src/imageEditor/LayersPanel.tsx +143 -0
- package/src/imageEditor/PropertiesPanel.tsx +428 -0
- package/src/imageEditor/Toolbar.tsx +242 -0
- package/src/imageEditor/icons.tsx +144 -0
- package/src/imageEditor/image-editor.css +450 -0
- package/src/imageEditor/layers/EditorImageLayer.tsx +45 -0
- package/src/imageEditor/layers/EditorShapeLayer.tsx +62 -0
- package/src/imageEditor/layers/EditorTextLayer.tsx +45 -0
- package/src/imageEditor/layers/SelectionHandles.tsx +86 -0
- package/src/imageEditor/state.ts +153 -0
- package/src/imageEditor/useImageEditor.ts +328 -0
- package/src/imageEditor/useImageEditorTokens.ts +70 -0
- package/src/index.ts +82 -0
- package/src/jsonEditor/EmbeddedRichTextField.tsx +81 -0
- package/src/jsonEditor/JsonEditor.tsx +81 -0
- package/src/jsonEditor/JsonEditorContext.tsx +75 -0
- package/src/jsonEditor/RenderNode.tsx +66 -0
- package/src/jsonEditor/editors.tsx +678 -0
- package/src/jsonEditor/index.ts +2 -0
- package/src/jsonEditor/json-editor.css +463 -0
- package/src/jsonEditor/useJsonEditorTokens.ts +63 -0
- package/src/recorder/RecorderButton.tsx +72 -0
- package/src/recorder/RecorderModal.tsx +596 -0
- package/src/recorder/RecorderPanel.tsx +93 -0
- package/src/recorder/formats.ts +159 -0
- package/src/recorder/hooks/useMediaRecorder.ts +378 -0
- package/src/recorder/hooks/useStreamPreview.ts +47 -0
- package/src/recorder/sources/cameraStream.ts +32 -0
- package/src/recorder/sources/micStream.ts +25 -0
- package/src/recorder/sources/screenStream.ts +162 -0
- package/src/recorder/timingJson.ts +66 -0
- package/src/styles/editor.css +2490 -51
- package/src/styles/image-edit-affordance.css +201 -0
- package/src/styles/index.css +10 -0
- package/src/tiptap/TiptapAudio.tsx +86 -0
- package/src/tiptap/TiptapVideo.tsx +119 -0
- package/src/tiptap/useResolvedMediaSrc.ts +47 -0
- package/src/tiptapBridge.ts +188 -20
- package/src/useHeadingLayout.ts +294 -0
- package/src/utils/collectInlineFontAwesomeCss.ts +69 -0
- package/src/utils/dropUtils.ts +54 -6
- package/src/utils/normalizeMalformedAssetUrl.ts +22 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
buildFilename,
|
|
4
|
+
resolveFormat,
|
|
5
|
+
supportsDisplayMedia,
|
|
6
|
+
supportsMediaRecorder,
|
|
7
|
+
supportsUserMedia,
|
|
8
|
+
} from '../recorder/formats.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Most of these probes touch global APIs that jsdom doesn't implement.
|
|
12
|
+
* Each test installs the minimum stub needed to exercise the probe and
|
|
13
|
+
* cleans up after itself so probes in other suites stay independent.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const originalMediaRecorder = (globalThis as { MediaRecorder?: unknown }).MediaRecorder;
|
|
17
|
+
const originalNavigator = globalThis.navigator;
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
if (originalMediaRecorder === undefined) {
|
|
21
|
+
delete (globalThis as { MediaRecorder?: unknown }).MediaRecorder;
|
|
22
|
+
} else {
|
|
23
|
+
(globalThis as { MediaRecorder?: unknown }).MediaRecorder = originalMediaRecorder;
|
|
24
|
+
}
|
|
25
|
+
Object.defineProperty(globalThis, 'navigator', {
|
|
26
|
+
value: originalNavigator,
|
|
27
|
+
configurable: true,
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
function installMediaRecorderStub(supported: readonly string[]) {
|
|
32
|
+
const stub = {
|
|
33
|
+
isTypeSupported: (mime: string) => supported.includes(mime),
|
|
34
|
+
};
|
|
35
|
+
(globalThis as { MediaRecorder?: unknown }).MediaRecorder = stub;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('supportsMediaRecorder', () => {
|
|
39
|
+
it('is false when MediaRecorder is undefined', () => {
|
|
40
|
+
delete (globalThis as { MediaRecorder?: unknown }).MediaRecorder;
|
|
41
|
+
expect(supportsMediaRecorder()).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('is true when a MediaRecorder global exists', () => {
|
|
45
|
+
installMediaRecorderStub(['audio/webm']);
|
|
46
|
+
expect(supportsMediaRecorder()).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('supportsUserMedia / supportsDisplayMedia', () => {
|
|
51
|
+
it('is false when navigator.mediaDevices is unavailable', () => {
|
|
52
|
+
Object.defineProperty(globalThis, 'navigator', {
|
|
53
|
+
value: {},
|
|
54
|
+
configurable: true,
|
|
55
|
+
});
|
|
56
|
+
expect(supportsUserMedia()).toBe(false);
|
|
57
|
+
expect(supportsDisplayMedia()).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('is true only for the methods that exist', () => {
|
|
61
|
+
Object.defineProperty(globalThis, 'navigator', {
|
|
62
|
+
value: { mediaDevices: { getUserMedia: vi.fn() } },
|
|
63
|
+
configurable: true,
|
|
64
|
+
});
|
|
65
|
+
expect(supportsUserMedia()).toBe(true);
|
|
66
|
+
expect(supportsDisplayMedia()).toBe(false);
|
|
67
|
+
|
|
68
|
+
Object.defineProperty(globalThis, 'navigator', {
|
|
69
|
+
value: { mediaDevices: { getUserMedia: vi.fn(), getDisplayMedia: vi.fn() } },
|
|
70
|
+
configurable: true,
|
|
71
|
+
});
|
|
72
|
+
expect(supportsUserMedia()).toBe(true);
|
|
73
|
+
expect(supportsDisplayMedia()).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('resolveFormat', () => {
|
|
78
|
+
it('picks the first supported audio candidate', () => {
|
|
79
|
+
installMediaRecorderStub(['audio/webm;codecs=opus', 'audio/webm']);
|
|
80
|
+
const fmt = resolveFormat('audio');
|
|
81
|
+
expect(fmt.mimeType).toBe('audio/webm;codecs=opus');
|
|
82
|
+
expect(fmt.extension).toBe('.webm');
|
|
83
|
+
expect(fmt.directory).toBe('audio');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('picks the first supported video candidate', () => {
|
|
87
|
+
installMediaRecorderStub(['video/mp4;codecs=avc1.42E01E,mp4a.40.2']);
|
|
88
|
+
const fmt = resolveFormat('video');
|
|
89
|
+
expect(fmt.mimeType).toBe('video/mp4;codecs=avc1.42E01E,mp4a.40.2');
|
|
90
|
+
expect(fmt.extension).toBe('.mp4');
|
|
91
|
+
expect(fmt.directory).toBe('video');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('honors a caller-supplied preferred MIME when supported', () => {
|
|
95
|
+
installMediaRecorderStub(['audio/webm', 'audio/ogg;codecs=opus']);
|
|
96
|
+
const fmt = resolveFormat('audio', 'audio/ogg;codecs=opus');
|
|
97
|
+
expect(fmt.mimeType).toBe('audio/ogg;codecs=opus');
|
|
98
|
+
expect(fmt.extension).toBe('.ogg');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('falls through the candidate list when the preferred MIME is unsupported', () => {
|
|
102
|
+
installMediaRecorderStub(['audio/webm']);
|
|
103
|
+
const fmt = resolveFormat('audio', 'audio/aac');
|
|
104
|
+
expect(fmt.mimeType).toBe('audio/webm');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('returns an empty MIME with a .webm extension fallback when nothing matches', () => {
|
|
108
|
+
installMediaRecorderStub([]);
|
|
109
|
+
const fmt = resolveFormat('video');
|
|
110
|
+
expect(fmt.mimeType).toBe('');
|
|
111
|
+
expect(fmt.extension).toBe('.webm');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('returns the .webm fallback extension when MediaRecorder is absent entirely', () => {
|
|
115
|
+
delete (globalThis as { MediaRecorder?: unknown }).MediaRecorder;
|
|
116
|
+
const fmt = resolveFormat('audio');
|
|
117
|
+
expect(fmt.mimeType).toBe('');
|
|
118
|
+
expect(fmt.extension).toBe('.webm');
|
|
119
|
+
expect(fmt.directory).toBe('audio');
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('buildFilename', () => {
|
|
124
|
+
it('uses the basename when supplied, prefixed with the chosen extension', () => {
|
|
125
|
+
expect(buildFilename('audio', '.webm', 'intro')).toBe('intro.webm');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('sanitizes filesystem-hostile characters', () => {
|
|
129
|
+
expect(buildFilename('video', '.mp4', 'my recording: take/2?')).toBe(
|
|
130
|
+
'my-recording--take-2-.mp4',
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('falls back to a timestamped name when no basename is supplied', () => {
|
|
135
|
+
const name = buildFilename('audio', '.webm');
|
|
136
|
+
expect(name.startsWith('narration-')).toBe(true);
|
|
137
|
+
expect(name.endsWith('.webm')).toBe(true);
|
|
138
|
+
// Format is narration-YYYYMMDD-HHMMSS.webm — 15 chars after the prefix.
|
|
139
|
+
expect(name).toMatch(/^narration-\d{8}-\d{6}\.webm$/);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('uses the recording-* prefix for video sources', () => {
|
|
143
|
+
const name = buildFilename('video', '.mp4');
|
|
144
|
+
expect(name).toMatch(/^recording-\d{8}-\d{6}\.mp4$/);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { buildTimingJson, encodeTimingJson, timingPathFor } from '../recorder/timingJson.js';
|
|
3
|
+
|
|
4
|
+
describe('buildTimingJson', () => {
|
|
5
|
+
it('returns a payload matching what resolveAudioMapping() expects', () => {
|
|
6
|
+
const timing = buildTimingJson('Hello world.', 12.5);
|
|
7
|
+
expect(timing).toEqual({
|
|
8
|
+
sourceText: 'Hello world.',
|
|
9
|
+
duration: 12.5,
|
|
10
|
+
bookmarks: [],
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('coerces missing or negative durations to 0', () => {
|
|
15
|
+
expect(buildTimingJson('script', Number.NaN).duration).toBe(0);
|
|
16
|
+
expect(buildTimingJson('script', -5).duration).toBe(0);
|
|
17
|
+
expect(buildTimingJson('script', Number.POSITIVE_INFINITY).duration).toBe(0);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('treats empty sourceText as an empty string, not undefined', () => {
|
|
21
|
+
expect(buildTimingJson('', 1).sourceText).toBe('');
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('encodeTimingJson', () => {
|
|
26
|
+
it('emits valid pretty-printed JSON', () => {
|
|
27
|
+
const bytes = encodeTimingJson(buildTimingJson('Hi.', 3));
|
|
28
|
+
const text = new TextDecoder().decode(bytes);
|
|
29
|
+
const parsed: unknown = JSON.parse(text);
|
|
30
|
+
expect(parsed).toEqual({ sourceText: 'Hi.', duration: 3, bookmarks: [] });
|
|
31
|
+
// Pretty-printed means newlines and indentation are present.
|
|
32
|
+
expect(text).toContain('\n');
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('timingPathFor', () => {
|
|
37
|
+
it('appends .timing.json to the audio path verbatim', () => {
|
|
38
|
+
expect(timingPathFor('audio/narration.webm')).toBe('audio/narration.webm.timing.json');
|
|
39
|
+
expect(timingPathFor('intro.mp3')).toBe('intro.mp3.timing.json');
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { markdownToTiptap, tiptapToMarkdown } from '../tiptapBridge';
|
|
3
|
+
|
|
4
|
+
describe('Template annotation round-trip', () => {
|
|
5
|
+
it('extracts {[template]} into data-template on markdownToTiptap', () => {
|
|
6
|
+
const html = markdownToTiptap('## Getting Started {[comparisonBar]}');
|
|
7
|
+
expect(html).toContain('data-template="comparisonBar"');
|
|
8
|
+
expect(html).toContain('Getting Started');
|
|
9
|
+
expect(html).not.toContain('{[comparisonBar]}'); // raw text stripped
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('round-trips markdown → HTML → markdown losslessly', () => {
|
|
13
|
+
const original = '## Getting Started {[comparisonBar]}';
|
|
14
|
+
const html = markdownToTiptap(original);
|
|
15
|
+
const back = tiptapToMarkdown(html);
|
|
16
|
+
expect(back.trim()).toBe(original);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('preserves template annotation through Tiptap-rendered HTML (with badge spans)', () => {
|
|
20
|
+
// Simulates HTML that Tiptap actually renders after parse: includes the
|
|
21
|
+
// squisq-heading-content + squisq-template-badge wrapper spans.
|
|
22
|
+
const tiptapRendered =
|
|
23
|
+
'<h2 data-template="comparisonBar">' +
|
|
24
|
+
'<span class="squisq-heading-content">Getting Started</span>' +
|
|
25
|
+
'<span class="squisq-template-badge" contenteditable="false" data-template="comparisonBar" data-template-label="Comparison Bar"></span>' +
|
|
26
|
+
'</h2>';
|
|
27
|
+
const md = tiptapToMarkdown(tiptapRendered);
|
|
28
|
+
expect(md).toContain('## Getting Started');
|
|
29
|
+
expect(md).toContain('{[comparisonBar]}');
|
|
30
|
+
// No literal badge spans should leak into markdown
|
|
31
|
+
expect(md).not.toContain('squisq-template-badge');
|
|
32
|
+
expect(md).not.toContain('Comparison Bar'); // CSS-rendered label shouldn't bleed
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -74,6 +74,21 @@ describe('markdownToTiptap', () => {
|
|
|
74
74
|
expect(back.trim()).toBe(md);
|
|
75
75
|
});
|
|
76
76
|
|
|
77
|
+
it('converts FontAwesome inline icons to <i data-icon> tags', () => {
|
|
78
|
+
const html = markdownToTiptap('{[angellist]}');
|
|
79
|
+
expect(html).toContain('data-icon="angellist"');
|
|
80
|
+
expect(html).toContain('data-family="brands"');
|
|
81
|
+
expect(html).toContain('data-name="angellist"');
|
|
82
|
+
expect(html).toContain('class="fa-brands fa-angellist"');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('round-trips FA inline icons through markdown↔HTML', () => {
|
|
86
|
+
const md = 'Repo {[github]} and {[angellist]}.';
|
|
87
|
+
const html = markdownToTiptap(md);
|
|
88
|
+
const back = tiptapToMarkdown(html);
|
|
89
|
+
expect(back.trim()).toBe(md);
|
|
90
|
+
});
|
|
91
|
+
|
|
77
92
|
it('converts images', () => {
|
|
78
93
|
const html = markdownToTiptap('');
|
|
79
94
|
expect(html).toContain('alt="Logo"');
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
5
|
+
import { renderHook, act, waitFor } from '@testing-library/react';
|
|
6
|
+
import { MemoryContentContainer } from '@bendyline/squisq/storage';
|
|
7
|
+
import {
|
|
8
|
+
IMAGE_EDIT_STATE_FILENAME,
|
|
9
|
+
readImageEditDoc,
|
|
10
|
+
writeImageEditDoc,
|
|
11
|
+
createEmptyImageEditDoc,
|
|
12
|
+
} from '@bendyline/squisq/imageEdit';
|
|
13
|
+
import { useImageEditor } from '../imageEditor/useImageEditor.js';
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
// jsdom doesn't implement createObjectURL or HTMLImageElement loading.
|
|
17
|
+
// Stub both so the hook can complete its asset-resolution path.
|
|
18
|
+
if (typeof URL.createObjectURL !== 'function') {
|
|
19
|
+
Object.defineProperty(URL, 'createObjectURL', {
|
|
20
|
+
configurable: true,
|
|
21
|
+
value: vi.fn(() => 'blob:stub'),
|
|
22
|
+
});
|
|
23
|
+
} else {
|
|
24
|
+
vi.spyOn(URL, 'createObjectURL').mockImplementation(() => 'blob:stub');
|
|
25
|
+
}
|
|
26
|
+
if (typeof URL.revokeObjectURL !== 'function') {
|
|
27
|
+
Object.defineProperty(URL, 'revokeObjectURL', {
|
|
28
|
+
configurable: true,
|
|
29
|
+
value: vi.fn(),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('useImageEditor', () => {
|
|
35
|
+
it('loads an existing state.json from the container', async () => {
|
|
36
|
+
const container = new MemoryContentContainer();
|
|
37
|
+
const doc = createEmptyImageEditDoc(120, 80);
|
|
38
|
+
await writeImageEditDoc(container, doc);
|
|
39
|
+
|
|
40
|
+
const { result } = renderHook(() => useImageEditor({ container, persistDebounceMs: 5 }));
|
|
41
|
+
|
|
42
|
+
await waitFor(() => expect(result.current.ready).toBe(true));
|
|
43
|
+
expect(result.current.state?.doc.canvas.width).toBe(120);
|
|
44
|
+
expect(result.current.error).toBe(null);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('seeds an empty doc when no state.json and no initialSrc', async () => {
|
|
48
|
+
const container = new MemoryContentContainer();
|
|
49
|
+
const { result } = renderHook(() => useImageEditor({ container, persistDebounceMs: 5 }));
|
|
50
|
+
|
|
51
|
+
await waitFor(() => expect(result.current.ready).toBe(true));
|
|
52
|
+
expect(result.current.state?.doc.layers).toHaveLength(0);
|
|
53
|
+
// The seed should have been written back to the container.
|
|
54
|
+
const persisted = await readImageEditDoc(container);
|
|
55
|
+
expect(persisted).not.toBeNull();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('persists state.json after a debounced action', async () => {
|
|
59
|
+
const container = new MemoryContentContainer();
|
|
60
|
+
await writeImageEditDoc(container, createEmptyImageEditDoc(50, 50));
|
|
61
|
+
|
|
62
|
+
const { result } = renderHook(() => useImageEditor({ container, persistDebounceMs: 5 }));
|
|
63
|
+
await waitFor(() => expect(result.current.ready).toBe(true));
|
|
64
|
+
|
|
65
|
+
act(() => {
|
|
66
|
+
result.current.dispatch({
|
|
67
|
+
type: 'add-layer',
|
|
68
|
+
layer: {
|
|
69
|
+
type: 'shape',
|
|
70
|
+
position: { x: 0, y: 0, width: 10, height: 10 },
|
|
71
|
+
content: { shape: 'rect', fill: '#000' },
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
await waitFor(() => expect(result.current.state?.dirty).toBe(false), {
|
|
77
|
+
timeout: 500,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const persisted = await readImageEditDoc(container);
|
|
81
|
+
expect(persisted?.layers).toHaveLength(1);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('uploadAsset writes bytes under assets/ and returns the path', async () => {
|
|
85
|
+
const container = new MemoryContentContainer();
|
|
86
|
+
await writeImageEditDoc(container, createEmptyImageEditDoc(10, 10));
|
|
87
|
+
|
|
88
|
+
const { result } = renderHook(() => useImageEditor({ container, persistDebounceMs: 5 }));
|
|
89
|
+
await waitFor(() => expect(result.current.ready).toBe(true));
|
|
90
|
+
|
|
91
|
+
const bytes = new Uint8Array([1, 2, 3, 4]);
|
|
92
|
+
// jsdom's Blob predates the arrayBuffer() method on some versions, so
|
|
93
|
+
// shim a minimal Blob-like that the hook only needs `.arrayBuffer()`
|
|
94
|
+
// and `.type` from.
|
|
95
|
+
const file = {
|
|
96
|
+
type: 'image/png',
|
|
97
|
+
arrayBuffer: async () => bytes.buffer,
|
|
98
|
+
} as unknown as Blob;
|
|
99
|
+
let path = '';
|
|
100
|
+
await act(async () => {
|
|
101
|
+
path = await result.current.uploadAsset(file, 'pic.png');
|
|
102
|
+
});
|
|
103
|
+
expect(path).toMatch(/^assets\/.+\.png$/);
|
|
104
|
+
const written = await container.readFile(path);
|
|
105
|
+
expect(written).not.toBeNull();
|
|
106
|
+
expect(written!.byteLength).toBe(4);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('flush writes synchronously and clears dirty', async () => {
|
|
110
|
+
const container = new MemoryContentContainer();
|
|
111
|
+
await writeImageEditDoc(container, createEmptyImageEditDoc(10, 10));
|
|
112
|
+
|
|
113
|
+
const { result } = renderHook(
|
|
114
|
+
() => useImageEditor({ container, persistDebounceMs: 100000 }), // effectively disabled
|
|
115
|
+
);
|
|
116
|
+
await waitFor(() => expect(result.current.ready).toBe(true));
|
|
117
|
+
|
|
118
|
+
act(() => {
|
|
119
|
+
result.current.dispatch({
|
|
120
|
+
type: 'set-canvas',
|
|
121
|
+
canvas: { width: 25, height: 25 },
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
expect(result.current.state?.dirty).toBe(true);
|
|
125
|
+
|
|
126
|
+
await act(async () => {
|
|
127
|
+
await result.current.flush();
|
|
128
|
+
});
|
|
129
|
+
expect(result.current.state?.dirty).toBe(false);
|
|
130
|
+
const persisted = await readImageEditDoc(container);
|
|
131
|
+
expect(persisted?.canvas.width).toBe(25);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('versioning is null when allowVersioning is false', async () => {
|
|
135
|
+
const container = new MemoryContentContainer();
|
|
136
|
+
await writeImageEditDoc(container, createEmptyImageEditDoc(10, 10));
|
|
137
|
+
const { result } = renderHook(() => useImageEditor({ container, persistDebounceMs: 5 }));
|
|
138
|
+
await waitFor(() => expect(result.current.ready).toBe(true));
|
|
139
|
+
expect(result.current.versioning).toBe(null);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('versioning manager is exposed when allowVersioning is true', async () => {
|
|
143
|
+
const container = new MemoryContentContainer();
|
|
144
|
+
await writeImageEditDoc(container, createEmptyImageEditDoc(10, 10));
|
|
145
|
+
const { result } = renderHook(() =>
|
|
146
|
+
useImageEditor({
|
|
147
|
+
container,
|
|
148
|
+
persistDebounceMs: 5,
|
|
149
|
+
allowVersioning: true,
|
|
150
|
+
versioningAutoSaveIdleMs: 0, // disable autosave
|
|
151
|
+
}),
|
|
152
|
+
);
|
|
153
|
+
await waitFor(() => expect(result.current.ready).toBe(true));
|
|
154
|
+
expect(result.current.versioning).not.toBe(null);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Ensure constants are referenced so unused-import lint stays quiet.
|
|
159
|
+
void IMAGE_EDIT_STATE_FILENAME;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { act, renderHook } from '@testing-library/react';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { useMediaRecorder } from '../recorder/hooks/useMediaRecorder.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Lifecycle test for the recorder hook with stubbed browser APIs.
|
|
7
|
+
* Drives request → start → stop and verifies the surface contract:
|
|
8
|
+
* state transitions, blob production, and cleanup on cancel.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
class FakeMediaStreamTrack {
|
|
12
|
+
readyState: 'live' | 'ended' = 'live';
|
|
13
|
+
kind: 'audio' | 'video';
|
|
14
|
+
stop = vi.fn(() => {
|
|
15
|
+
this.readyState = 'ended';
|
|
16
|
+
});
|
|
17
|
+
constructor(kind: 'audio' | 'video') {
|
|
18
|
+
this.kind = kind;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
class FakeMediaStream {
|
|
23
|
+
tracks: FakeMediaStreamTrack[];
|
|
24
|
+
constructor(tracks: FakeMediaStreamTrack[] = []) {
|
|
25
|
+
this.tracks = tracks;
|
|
26
|
+
}
|
|
27
|
+
get active() {
|
|
28
|
+
return this.tracks.some((t) => t.readyState === 'live');
|
|
29
|
+
}
|
|
30
|
+
getTracks() {
|
|
31
|
+
return this.tracks;
|
|
32
|
+
}
|
|
33
|
+
getAudioTracks() {
|
|
34
|
+
return this.tracks.filter((t) => t.kind === 'audio');
|
|
35
|
+
}
|
|
36
|
+
getVideoTracks() {
|
|
37
|
+
return this.tracks.filter((t) => t.kind === 'video');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface FakeRecorderHandle {
|
|
42
|
+
state: 'recording' | 'inactive';
|
|
43
|
+
mimeType: string;
|
|
44
|
+
stream: FakeMediaStream;
|
|
45
|
+
ondataavailable: ((event: { data: Blob }) => void) | null;
|
|
46
|
+
onstop: (() => void) | null;
|
|
47
|
+
onerror: ((event: unknown) => void) | null;
|
|
48
|
+
start(): void;
|
|
49
|
+
stop(): void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let lastRecorder: FakeRecorderHandle | null = null;
|
|
53
|
+
|
|
54
|
+
class FakeMediaRecorder implements FakeRecorderHandle {
|
|
55
|
+
state: 'recording' | 'inactive' = 'inactive';
|
|
56
|
+
mimeType: string;
|
|
57
|
+
stream: FakeMediaStream;
|
|
58
|
+
ondataavailable: ((event: { data: Blob }) => void) | null = null;
|
|
59
|
+
onstop: (() => void) | null = null;
|
|
60
|
+
onerror: ((event: unknown) => void) | null = null;
|
|
61
|
+
constructor(stream: FakeMediaStream, options?: { mimeType?: string }) {
|
|
62
|
+
this.stream = stream;
|
|
63
|
+
this.mimeType = options?.mimeType ?? 'audio/webm';
|
|
64
|
+
// Expose the most recent instance to the test body so assertions can
|
|
65
|
+
// poke at its state/event handlers. Not a `const self = this` alias
|
|
66
|
+
// pattern — `lastRecorder` is a module-level slot, not a workaround
|
|
67
|
+
// for arrow-function-vs-method `this` confusion.
|
|
68
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
69
|
+
lastRecorder = this;
|
|
70
|
+
}
|
|
71
|
+
static isTypeSupported(mime: string): boolean {
|
|
72
|
+
return mime.startsWith('audio/webm') || mime.startsWith('video/webm');
|
|
73
|
+
}
|
|
74
|
+
start() {
|
|
75
|
+
this.state = 'recording';
|
|
76
|
+
}
|
|
77
|
+
stop() {
|
|
78
|
+
if (this.state === 'inactive') return;
|
|
79
|
+
this.state = 'inactive';
|
|
80
|
+
// Emit a fake data chunk then resolve.
|
|
81
|
+
this.ondataavailable?.({ data: new Blob(['hello'], { type: this.mimeType }) });
|
|
82
|
+
this.onstop?.();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const originalMediaRecorder = (globalThis as { MediaRecorder?: unknown }).MediaRecorder;
|
|
87
|
+
const originalNavigator = globalThis.navigator;
|
|
88
|
+
|
|
89
|
+
beforeEach(() => {
|
|
90
|
+
lastRecorder = null;
|
|
91
|
+
(globalThis as { MediaRecorder?: unknown }).MediaRecorder = FakeMediaRecorder;
|
|
92
|
+
const fakeStream = new FakeMediaStream([new FakeMediaStreamTrack('audio')]);
|
|
93
|
+
Object.defineProperty(globalThis, 'navigator', {
|
|
94
|
+
value: {
|
|
95
|
+
mediaDevices: {
|
|
96
|
+
getUserMedia: vi.fn().mockResolvedValue(fakeStream),
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
configurable: true,
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
afterEach(() => {
|
|
104
|
+
if (originalMediaRecorder === undefined) {
|
|
105
|
+
delete (globalThis as { MediaRecorder?: unknown }).MediaRecorder;
|
|
106
|
+
} else {
|
|
107
|
+
(globalThis as { MediaRecorder?: unknown }).MediaRecorder = originalMediaRecorder;
|
|
108
|
+
}
|
|
109
|
+
Object.defineProperty(globalThis, 'navigator', {
|
|
110
|
+
value: originalNavigator,
|
|
111
|
+
configurable: true,
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('useMediaRecorder lifecycle', () => {
|
|
116
|
+
it('walks idle → ready → recording → stopped and produces a blob', async () => {
|
|
117
|
+
const { result } = renderHook(() => useMediaRecorder({ source: 'mic' }));
|
|
118
|
+
|
|
119
|
+
expect(result.current.state).toBe('idle');
|
|
120
|
+
|
|
121
|
+
await act(async () => {
|
|
122
|
+
await result.current.request();
|
|
123
|
+
});
|
|
124
|
+
expect(result.current.state).toBe('ready');
|
|
125
|
+
expect(result.current.stream).not.toBeNull();
|
|
126
|
+
expect(result.current.mimeType).toMatch(/^audio\/webm/);
|
|
127
|
+
expect(result.current.extension).toBe('.webm');
|
|
128
|
+
expect(result.current.directory).toBe('audio');
|
|
129
|
+
|
|
130
|
+
act(() => {
|
|
131
|
+
result.current.start();
|
|
132
|
+
});
|
|
133
|
+
expect(result.current.state).toBe('recording');
|
|
134
|
+
expect(lastRecorder?.state).toBe('recording');
|
|
135
|
+
|
|
136
|
+
let blob: Blob | null = null;
|
|
137
|
+
await act(async () => {
|
|
138
|
+
blob = await result.current.stop();
|
|
139
|
+
});
|
|
140
|
+
expect(result.current.state).toBe('stopped');
|
|
141
|
+
expect(blob).toBeInstanceOf(Blob);
|
|
142
|
+
expect(result.current.blob).toBeInstanceOf(Blob);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('reset() returns to ready when the underlying stream is still live (discard & re-record)', async () => {
|
|
146
|
+
const { result } = renderHook(() => useMediaRecorder({ source: 'mic' }));
|
|
147
|
+
|
|
148
|
+
await act(async () => {
|
|
149
|
+
await result.current.request();
|
|
150
|
+
});
|
|
151
|
+
act(() => {
|
|
152
|
+
result.current.start();
|
|
153
|
+
});
|
|
154
|
+
await act(async () => {
|
|
155
|
+
await result.current.stop();
|
|
156
|
+
});
|
|
157
|
+
expect(result.current.state).toBe('stopped');
|
|
158
|
+
|
|
159
|
+
act(() => {
|
|
160
|
+
result.current.reset();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
expect(result.current.state).toBe('ready');
|
|
164
|
+
expect(result.current.blob).toBeNull();
|
|
165
|
+
expect(result.current.stream).not.toBeNull();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('cancel() tears down state and stops the stream tracks', async () => {
|
|
169
|
+
const { result } = renderHook(() => useMediaRecorder({ source: 'mic' }));
|
|
170
|
+
|
|
171
|
+
await act(async () => {
|
|
172
|
+
await result.current.request();
|
|
173
|
+
});
|
|
174
|
+
const stream = result.current.stream as unknown as FakeMediaStream;
|
|
175
|
+
expect(stream).not.toBeNull();
|
|
176
|
+
const tracks = stream.getTracks();
|
|
177
|
+
|
|
178
|
+
act(() => {
|
|
179
|
+
result.current.cancel();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
expect(result.current.state).toBe('idle');
|
|
183
|
+
expect(result.current.stream).toBeNull();
|
|
184
|
+
expect(tracks.every((t) => t.stop.mock.calls.length > 0)).toBe(true);
|
|
185
|
+
});
|
|
186
|
+
});
|