@bendyline/squisq-editor-react 2.1.0 → 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/dist/index.d.ts +188 -55
- package/dist/index.js +5094 -2948
- package/dist/monaco.js +0 -1
- package/dist/styles/index.css +234 -145
- package/package.json +9 -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 -1274
- package/src/EditorShell.tsx +0 -1358
- 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 -1265
- package/src/PreviewPanel.tsx +0 -426
- package/src/RawEditor.tsx +0 -743
- package/src/RecorderEntry.tsx +0 -162
- 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 -2885
- 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 -806
- 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 -65
- 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 -550
- 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__/findMode.test.tsx +0 -104
- package/src/__tests__/findModel.test.ts +0 -55
- 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__/markdownCodeFence.test.ts +0 -45
- package/src/__tests__/mediaAttachmentFlow.test.ts +0 -110
- package/src/__tests__/mediaBinDrop.test.tsx +0 -90
- package/src/__tests__/mediaReferences.test.ts +0 -97
- 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 -571
- 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 -575
- 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 -190
- 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__/writeCanvasSettings.test.ts +0 -15
- 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 -164
- 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 -407
- 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/codeSnippet/CodeSnippetExtension.ts +0 -205
- package/src/codeSnippet/CodeSnippetWidget.tsx +0 -109
- package/src/codeSnippet/__tests__/CodeSnippetExtension.test.ts +0 -95
- package/src/codeSnippet/__tests__/codeSnippetLanguages.test.ts +0 -50
- package/src/codeSnippet/codeSnippetCommands.ts +0 -21
- package/src/codeSnippet/codeSnippetData.ts +0 -43
- package/src/codeSnippet/codeSnippetLanguages.ts +0 -216
- 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 -260
- 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/find/FindHighlightExtension.ts +0 -81
- package/src/find/FindToolbar.tsx +0 -314
- package/src/find/findModel.ts +0 -77
- 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 -559
- 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/markdownCodeFence.ts +0 -72
- package/src/mediaDragMime.ts +0 -32
- package/src/mediaEntries.ts +0 -12
- package/src/mediaReferences.ts +0 -301
- package/src/mermaid/MermaidDiagramCanvas.tsx +0 -693
- package/src/mermaid/MermaidDiagramExtension.ts +0 -243
- package/src/mermaid/MermaidDiagramTypeThumbnail.tsx +0 -184
- package/src/mermaid/MermaidDiagramWidget.tsx +0 -653
- package/src/mermaid/MermaidShapePalette.tsx +0 -245
- package/src/mermaid/__tests__/MermaidDiagramExtension.test.ts +0 -361
- package/src/mermaid/__tests__/mermaidDiagramTypes.test.ts +0 -35
- package/src/mermaid/__tests__/mermaidRenderer.test.ts +0 -49
- package/src/mermaid/__tests__/mermaidSourceOps.test.ts +0 -213
- package/src/mermaid/__tests__/mermaidSyntax.test.ts +0 -71
- package/src/mermaid/mermaidCommands.ts +0 -34
- package/src/mermaid/mermaidData.ts +0 -31
- package/src/mermaid/mermaidDiagramTypes.ts +0 -325
- package/src/mermaid/mermaidModel.ts +0 -31
- package/src/mermaid/mermaidRenderer.ts +0 -181
- package/src/mermaid/mermaidShapes.ts +0 -113
- package/src/mermaid/mermaidSourceOps.ts +0 -454
- 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 -647
- package/src/scene/SceneBlockExtension.ts +0 -205
- package/src/scene/SceneBlockToolbar.tsx +0 -216
- 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/SceneViewControls.tsx +0 -43
- 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__/fitScale.test.ts +0 -19
- 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 -130
- 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/fitScale.ts +0 -17
- package/src/scene/hooks/useSceneHitTest.ts +0 -105
- package/src/scene/hooks/useScenePanZoom.ts +0 -154
- 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 -874
- 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/code-snippet.css +0 -76
- package/src/styles/diagram.css +0 -183
- package/src/styles/editor.css +0 -7131
- package/src/styles/folder-view.css +0 -210
- package/src/styles/image-edit-affordance.css +0 -201
- package/src/styles/index.css +0 -20
- package/src/styles/mermaid-diagram.css +0 -487
- 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/writeCanvasSettings.ts +0 -30
- package/src/wysiwygImageUpload.ts +0 -113
|
@@ -1,226 +0,0 @@
|
|
|
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
|
-
function deferred<T>() {
|
|
16
|
-
let resolve!: (value: T) => void;
|
|
17
|
-
const promise = new Promise<T>((res) => {
|
|
18
|
-
resolve = res;
|
|
19
|
-
});
|
|
20
|
-
return { promise, resolve };
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
beforeEach(() => {
|
|
24
|
-
// jsdom doesn't implement createObjectURL or HTMLImageElement loading.
|
|
25
|
-
// Stub both so the hook can complete its asset-resolution path.
|
|
26
|
-
if (typeof URL.createObjectURL !== 'function') {
|
|
27
|
-
Object.defineProperty(URL, 'createObjectURL', {
|
|
28
|
-
configurable: true,
|
|
29
|
-
value: vi.fn(() => 'blob:stub'),
|
|
30
|
-
});
|
|
31
|
-
} else {
|
|
32
|
-
vi.spyOn(URL, 'createObjectURL').mockImplementation(() => 'blob:stub');
|
|
33
|
-
}
|
|
34
|
-
if (typeof URL.revokeObjectURL !== 'function') {
|
|
35
|
-
Object.defineProperty(URL, 'revokeObjectURL', {
|
|
36
|
-
configurable: true,
|
|
37
|
-
value: vi.fn(),
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
describe('useImageEditor', () => {
|
|
43
|
-
it('loads an existing state.json from the container', async () => {
|
|
44
|
-
const container = new MemoryContentContainer();
|
|
45
|
-
const doc = createEmptyImageEditDoc(120, 80);
|
|
46
|
-
await writeImageEditDoc(container, doc);
|
|
47
|
-
|
|
48
|
-
const { result } = renderHook(() => useImageEditor({ container, persistDebounceMs: 5 }));
|
|
49
|
-
|
|
50
|
-
await waitFor(() => expect(result.current.ready).toBe(true));
|
|
51
|
-
expect(result.current.state?.doc.canvas.width).toBe(120);
|
|
52
|
-
expect(result.current.error).toBe(null);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('seeds an empty doc when no state.json and no initialSrc', async () => {
|
|
56
|
-
const container = new MemoryContentContainer();
|
|
57
|
-
const { result } = renderHook(() => useImageEditor({ container, persistDebounceMs: 5 }));
|
|
58
|
-
|
|
59
|
-
await waitFor(() => expect(result.current.ready).toBe(true));
|
|
60
|
-
expect(result.current.state?.doc.layers).toHaveLength(0);
|
|
61
|
-
// The seed should have been written back to the container.
|
|
62
|
-
const persisted = await readImageEditDoc(container);
|
|
63
|
-
expect(persisted).not.toBeNull();
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('persists state.json after a debounced action', async () => {
|
|
67
|
-
const container = new MemoryContentContainer();
|
|
68
|
-
await writeImageEditDoc(container, createEmptyImageEditDoc(50, 50));
|
|
69
|
-
|
|
70
|
-
const { result } = renderHook(() => useImageEditor({ container, persistDebounceMs: 5 }));
|
|
71
|
-
await waitFor(() => expect(result.current.ready).toBe(true));
|
|
72
|
-
|
|
73
|
-
act(() => {
|
|
74
|
-
result.current.dispatch({
|
|
75
|
-
type: 'add-layer',
|
|
76
|
-
layer: {
|
|
77
|
-
type: 'shape',
|
|
78
|
-
position: { x: 0, y: 0, width: 10, height: 10 },
|
|
79
|
-
content: { shape: 'rect', fill: '#000' },
|
|
80
|
-
},
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
await waitFor(() => expect(result.current.state?.dirty).toBe(false), {
|
|
85
|
-
timeout: 500,
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
const persisted = await readImageEditDoc(container);
|
|
89
|
-
expect(persisted?.layers).toHaveLength(1);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('uploadAsset writes bytes under assets/ and returns the path', async () => {
|
|
93
|
-
const container = new MemoryContentContainer();
|
|
94
|
-
await writeImageEditDoc(container, createEmptyImageEditDoc(10, 10));
|
|
95
|
-
|
|
96
|
-
const { result } = renderHook(() => useImageEditor({ container, persistDebounceMs: 5 }));
|
|
97
|
-
await waitFor(() => expect(result.current.ready).toBe(true));
|
|
98
|
-
|
|
99
|
-
const bytes = new Uint8Array([1, 2, 3, 4]);
|
|
100
|
-
// jsdom's Blob predates the arrayBuffer() method on some versions, so
|
|
101
|
-
// shim a minimal Blob-like that the hook only needs `.arrayBuffer()`
|
|
102
|
-
// and `.type` from.
|
|
103
|
-
const file = {
|
|
104
|
-
type: 'image/png',
|
|
105
|
-
arrayBuffer: async () => bytes.buffer,
|
|
106
|
-
} as unknown as Blob;
|
|
107
|
-
let path = '';
|
|
108
|
-
await act(async () => {
|
|
109
|
-
path = await result.current.uploadAsset(file, 'pic.png');
|
|
110
|
-
});
|
|
111
|
-
expect(path).toMatch(/^assets\/.+\.png$/);
|
|
112
|
-
const written = await container.readFile(path);
|
|
113
|
-
expect(written).not.toBeNull();
|
|
114
|
-
expect(written!.byteLength).toBe(4);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('flush writes synchronously and clears dirty', async () => {
|
|
118
|
-
const container = new MemoryContentContainer();
|
|
119
|
-
await writeImageEditDoc(container, createEmptyImageEditDoc(10, 10));
|
|
120
|
-
|
|
121
|
-
const { result } = renderHook(
|
|
122
|
-
() => useImageEditor({ container, persistDebounceMs: 100000 }), // effectively disabled
|
|
123
|
-
);
|
|
124
|
-
await waitFor(() => expect(result.current.ready).toBe(true));
|
|
125
|
-
|
|
126
|
-
act(() => {
|
|
127
|
-
result.current.dispatch({
|
|
128
|
-
type: 'set-canvas',
|
|
129
|
-
canvas: { width: 25, height: 25 },
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
expect(result.current.state?.dirty).toBe(true);
|
|
133
|
-
|
|
134
|
-
await act(async () => {
|
|
135
|
-
await result.current.flush();
|
|
136
|
-
});
|
|
137
|
-
expect(result.current.state?.dirty).toBe(false);
|
|
138
|
-
const persisted = await readImageEditDoc(container);
|
|
139
|
-
expect(persisted?.canvas.width).toBe(25);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
it('does not mark a newer edit clean when an older write resolves', async () => {
|
|
143
|
-
const container = new MemoryContentContainer();
|
|
144
|
-
await writeImageEditDoc(container, createEmptyImageEditDoc(10, 10));
|
|
145
|
-
const originalWrite = container.writeFile.bind(container);
|
|
146
|
-
const gate = deferred<void>();
|
|
147
|
-
const started = deferred<void>();
|
|
148
|
-
let writes = 0;
|
|
149
|
-
vi.spyOn(container, 'writeFile').mockImplementation(async (...args) => {
|
|
150
|
-
writes += 1;
|
|
151
|
-
if (writes === 1) {
|
|
152
|
-
started.resolve();
|
|
153
|
-
await gate.promise;
|
|
154
|
-
}
|
|
155
|
-
return originalWrite(...args);
|
|
156
|
-
});
|
|
157
|
-
const { result } = renderHook(() => useImageEditor({ container, persistDebounceMs: 5 }));
|
|
158
|
-
await waitFor(() => expect(result.current.ready).toBe(true));
|
|
159
|
-
|
|
160
|
-
act(() => result.current.dispatch({ type: 'set-canvas', canvas: { width: 20, height: 20 } }));
|
|
161
|
-
await started.promise;
|
|
162
|
-
act(() => result.current.dispatch({ type: 'set-canvas', canvas: { width: 30, height: 30 } }));
|
|
163
|
-
gate.resolve();
|
|
164
|
-
|
|
165
|
-
await waitFor(() => expect(result.current.state?.dirty).toBe(false));
|
|
166
|
-
expect((await readImageEditDoc(container))?.canvas.width).toBe(30);
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
it('flushes a dirty debounce when the editor unmounts', async () => {
|
|
170
|
-
const container = new MemoryContentContainer();
|
|
171
|
-
await writeImageEditDoc(container, createEmptyImageEditDoc(10, 10));
|
|
172
|
-
const { result, unmount } = renderHook(() =>
|
|
173
|
-
useImageEditor({ container, persistDebounceMs: 100000 }),
|
|
174
|
-
);
|
|
175
|
-
await waitFor(() => expect(result.current.ready).toBe(true));
|
|
176
|
-
act(() => result.current.dispatch({ type: 'set-canvas', canvas: { width: 44, height: 44 } }));
|
|
177
|
-
unmount();
|
|
178
|
-
await waitFor(async () => expect((await readImageEditDoc(container))?.canvas.width).toBe(44));
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
it('does not populate the URL cache after unmount', async () => {
|
|
182
|
-
const container = new MemoryContentContainer();
|
|
183
|
-
await writeImageEditDoc(container, createEmptyImageEditDoc(10, 10));
|
|
184
|
-
await container.writeFile('assets/late.png', new Uint8Array([1]).buffer, 'image/png');
|
|
185
|
-
const { result, unmount } = renderHook(() => useImageEditor({ container }));
|
|
186
|
-
await waitFor(() => expect(result.current.ready).toBe(true));
|
|
187
|
-
const originalRead = container.readFile.bind(container);
|
|
188
|
-
const gate = deferred<ArrayBuffer | null>();
|
|
189
|
-
vi.spyOn(container, 'readFile').mockImplementation((path) =>
|
|
190
|
-
path === 'assets/late.png' ? gate.promise : originalRead(path),
|
|
191
|
-
);
|
|
192
|
-
const create = vi.mocked(URL.createObjectURL);
|
|
193
|
-
const before = create.mock.calls.length;
|
|
194
|
-
const pending = result.current.resolveAssetUrl('assets/late.png');
|
|
195
|
-
unmount();
|
|
196
|
-
gate.resolve(new Uint8Array([1]).buffer);
|
|
197
|
-
await expect(pending).rejects.toThrow(/cancelled/);
|
|
198
|
-
expect(create).toHaveBeenCalledTimes(before);
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
it('versioning is null when allowVersioning is false', async () => {
|
|
202
|
-
const container = new MemoryContentContainer();
|
|
203
|
-
await writeImageEditDoc(container, createEmptyImageEditDoc(10, 10));
|
|
204
|
-
const { result } = renderHook(() => useImageEditor({ container, persistDebounceMs: 5 }));
|
|
205
|
-
await waitFor(() => expect(result.current.ready).toBe(true));
|
|
206
|
-
expect(result.current.versioning).toBe(null);
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
it('versioning manager is exposed when allowVersioning is true', async () => {
|
|
210
|
-
const container = new MemoryContentContainer();
|
|
211
|
-
await writeImageEditDoc(container, createEmptyImageEditDoc(10, 10));
|
|
212
|
-
const { result } = renderHook(() =>
|
|
213
|
-
useImageEditor({
|
|
214
|
-
container,
|
|
215
|
-
persistDebounceMs: 5,
|
|
216
|
-
allowVersioning: true,
|
|
217
|
-
versioningAutoSaveIdleMs: 0, // disable autosave
|
|
218
|
-
}),
|
|
219
|
-
);
|
|
220
|
-
await waitFor(() => expect(result.current.ready).toBe(true));
|
|
221
|
-
expect(result.current.versioning).not.toBe(null);
|
|
222
|
-
});
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
// Ensure constants are referenced so unused-import lint stays quiet.
|
|
226
|
-
void IMAGE_EDIT_STATE_FILENAME;
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
-
import { renderHook } from '@testing-library/react';
|
|
3
|
-
import { DARK_SURFACE, LIGHT_SURFACE, DEFAULT_THEME } from '@bendyline/squisq/schemas';
|
|
4
|
-
import { useJsonEditorTokens } from '../jsonEditor/useJsonEditorTokens';
|
|
5
|
-
|
|
6
|
-
type StyleBag = Record<string, string>;
|
|
7
|
-
|
|
8
|
-
/** Install a matchMedia stub whose `(prefers-color-scheme: dark)` resolves to `dark`. */
|
|
9
|
-
function mockPrefersDark(dark: boolean): void {
|
|
10
|
-
Object.defineProperty(window, 'matchMedia', {
|
|
11
|
-
configurable: true,
|
|
12
|
-
value: (query: string) => ({
|
|
13
|
-
matches: dark && query.includes('dark'),
|
|
14
|
-
media: query,
|
|
15
|
-
onchange: null,
|
|
16
|
-
addListener: vi.fn(),
|
|
17
|
-
removeListener: vi.fn(),
|
|
18
|
-
addEventListener: vi.fn(),
|
|
19
|
-
removeEventListener: vi.fn(),
|
|
20
|
-
dispatchEvent: vi.fn(() => false),
|
|
21
|
-
}),
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
describe('useJsonEditorTokens', () => {
|
|
26
|
-
afterEach(() => {
|
|
27
|
-
vi.restoreAllMocks();
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it('emits jsonform-prefixed tokens', () => {
|
|
31
|
-
mockPrefersDark(false);
|
|
32
|
-
const { result } = renderHook(() => useJsonEditorTokens(DEFAULT_THEME, LIGHT_SURFACE));
|
|
33
|
-
const style = result.current.style as StyleBag;
|
|
34
|
-
expect(style['--squisq-jsonform-bg']).toBe(LIGHT_SURFACE.background);
|
|
35
|
-
expect(style).toHaveProperty('--squisq-jsonform-warning');
|
|
36
|
-
expect(style).toHaveProperty('--squisq-jsonform-input-bg');
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("responds to a dark OS preference under surface='auto' (reactive via useAutoSurface)", () => {
|
|
40
|
-
mockPrefersDark(true);
|
|
41
|
-
const { result } = renderHook(() => useJsonEditorTokens(DEFAULT_THEME, 'auto'));
|
|
42
|
-
const style = result.current.style as StyleBag;
|
|
43
|
-
expect(style['--squisq-jsonform-bg']).toBe(DARK_SURFACE.background);
|
|
44
|
-
expect(style['--squisq-jsonform-text']).toBe(DARK_SURFACE.text);
|
|
45
|
-
expect(result.current.theme.colors.background).toBe(DARK_SURFACE.background);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it("uses light surface under surface='auto' when the OS prefers light", () => {
|
|
49
|
-
mockPrefersDark(false);
|
|
50
|
-
const { result } = renderHook(() => useJsonEditorTokens(DEFAULT_THEME, 'auto'));
|
|
51
|
-
const style = result.current.style as StyleBag;
|
|
52
|
-
expect(style['--squisq-jsonform-bg']).toBe(LIGHT_SURFACE.background);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
beforeEach(() => {
|
|
56
|
-
// Default to a defined matchMedia so the hook's useAutoSurface can subscribe.
|
|
57
|
-
mockPrefersDark(false);
|
|
58
|
-
});
|
|
59
|
-
});
|
|
@@ -1,302 +0,0 @@
|
|
|
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
|
-
let lastStream: FakeMediaStream | null = null;
|
|
54
|
-
|
|
55
|
-
class FakeMediaRecorder implements FakeRecorderHandle {
|
|
56
|
-
state: 'recording' | 'inactive' = 'inactive';
|
|
57
|
-
mimeType: string;
|
|
58
|
-
stream: FakeMediaStream;
|
|
59
|
-
ondataavailable: ((event: { data: Blob }) => void) | null = null;
|
|
60
|
-
onstop: (() => void) | null = null;
|
|
61
|
-
onerror: ((event: unknown) => void) | null = null;
|
|
62
|
-
constructor(stream: FakeMediaStream, options?: { mimeType?: string }) {
|
|
63
|
-
this.stream = stream;
|
|
64
|
-
this.mimeType = options?.mimeType ?? 'audio/webm';
|
|
65
|
-
// Expose the most recent instance to the test body so assertions can
|
|
66
|
-
// poke at its state/event handlers. Not a `const self = this` alias
|
|
67
|
-
// pattern — `lastRecorder` is a module-level slot, not a workaround
|
|
68
|
-
// for arrow-function-vs-method `this` confusion.
|
|
69
|
-
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
70
|
-
lastRecorder = this;
|
|
71
|
-
}
|
|
72
|
-
static isTypeSupported(mime: string): boolean {
|
|
73
|
-
return mime.startsWith('audio/webm') || mime.startsWith('video/webm');
|
|
74
|
-
}
|
|
75
|
-
start() {
|
|
76
|
-
this.state = 'recording';
|
|
77
|
-
}
|
|
78
|
-
stop() {
|
|
79
|
-
if (this.state === 'inactive') return;
|
|
80
|
-
this.state = 'inactive';
|
|
81
|
-
// Emit a fake data chunk then resolve.
|
|
82
|
-
this.ondataavailable?.({ data: new Blob(['hello'], { type: this.mimeType }) });
|
|
83
|
-
this.onstop?.();
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const originalMediaRecorder = (globalThis as { MediaRecorder?: unknown }).MediaRecorder;
|
|
88
|
-
const originalNavigator = globalThis.navigator;
|
|
89
|
-
|
|
90
|
-
beforeEach(() => {
|
|
91
|
-
lastRecorder = null;
|
|
92
|
-
(globalThis as { MediaRecorder?: unknown }).MediaRecorder = FakeMediaRecorder;
|
|
93
|
-
const fakeStream = new FakeMediaStream([new FakeMediaStreamTrack('audio')]);
|
|
94
|
-
lastStream = fakeStream;
|
|
95
|
-
Object.defineProperty(globalThis, 'navigator', {
|
|
96
|
-
value: {
|
|
97
|
-
mediaDevices: {
|
|
98
|
-
getUserMedia: vi.fn().mockResolvedValue(fakeStream),
|
|
99
|
-
},
|
|
100
|
-
},
|
|
101
|
-
configurable: true,
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
afterEach(() => {
|
|
106
|
-
if (originalMediaRecorder === undefined) {
|
|
107
|
-
delete (globalThis as { MediaRecorder?: unknown }).MediaRecorder;
|
|
108
|
-
} else {
|
|
109
|
-
(globalThis as { MediaRecorder?: unknown }).MediaRecorder = originalMediaRecorder;
|
|
110
|
-
}
|
|
111
|
-
Object.defineProperty(globalThis, 'navigator', {
|
|
112
|
-
value: originalNavigator,
|
|
113
|
-
configurable: true,
|
|
114
|
-
});
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
describe('useMediaRecorder lifecycle', () => {
|
|
118
|
-
it('walks idle → ready → recording → stopped and produces a blob', async () => {
|
|
119
|
-
const { result } = renderHook(() => useMediaRecorder({ source: 'mic' }));
|
|
120
|
-
|
|
121
|
-
expect(result.current.state).toBe('idle');
|
|
122
|
-
|
|
123
|
-
await act(async () => {
|
|
124
|
-
await result.current.request();
|
|
125
|
-
});
|
|
126
|
-
expect(result.current.state).toBe('ready');
|
|
127
|
-
expect(result.current.stream).not.toBeNull();
|
|
128
|
-
expect(result.current.mimeType).toMatch(/^audio\/webm/);
|
|
129
|
-
expect(result.current.extension).toBe('.webm');
|
|
130
|
-
expect(result.current.directory).toBe('audio');
|
|
131
|
-
|
|
132
|
-
act(() => {
|
|
133
|
-
result.current.start();
|
|
134
|
-
});
|
|
135
|
-
expect(result.current.state).toBe('recording');
|
|
136
|
-
expect(lastRecorder?.state).toBe('recording');
|
|
137
|
-
|
|
138
|
-
let blob: Blob | null = null;
|
|
139
|
-
await act(async () => {
|
|
140
|
-
blob = await result.current.stop();
|
|
141
|
-
});
|
|
142
|
-
expect(result.current.state).toBe('stopped');
|
|
143
|
-
expect(blob).toBeInstanceOf(Blob);
|
|
144
|
-
expect(result.current.blob).toBeInstanceOf(Blob);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it('reset() returns to ready when the underlying stream is still live (discard & re-record)', async () => {
|
|
148
|
-
const { result } = renderHook(() => useMediaRecorder({ source: 'mic' }));
|
|
149
|
-
|
|
150
|
-
await act(async () => {
|
|
151
|
-
await result.current.request();
|
|
152
|
-
});
|
|
153
|
-
act(() => {
|
|
154
|
-
result.current.start();
|
|
155
|
-
});
|
|
156
|
-
await act(async () => {
|
|
157
|
-
await result.current.stop();
|
|
158
|
-
});
|
|
159
|
-
expect(result.current.state).toBe('stopped');
|
|
160
|
-
|
|
161
|
-
act(() => {
|
|
162
|
-
result.current.reset();
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
expect(result.current.state).toBe('ready');
|
|
166
|
-
expect(result.current.blob).toBeNull();
|
|
167
|
-
expect(result.current.stream).not.toBeNull();
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it('defaults to the mic source when called with no options', async () => {
|
|
171
|
-
const { result } = renderHook(() => useMediaRecorder());
|
|
172
|
-
|
|
173
|
-
expect(result.current.state).toBe('idle');
|
|
174
|
-
|
|
175
|
-
await act(async () => {
|
|
176
|
-
await result.current.request();
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
// Mic path: audio-only capture via getUserMedia, lands in `audio/`.
|
|
180
|
-
expect(result.current.state).toBe('ready');
|
|
181
|
-
expect(result.current.mimeType).toMatch(/^audio\/webm/);
|
|
182
|
-
expect(result.current.directory).toBe('audio');
|
|
183
|
-
const getUserMedia = navigator.mediaDevices.getUserMedia as ReturnType<typeof vi.fn>;
|
|
184
|
-
expect(getUserMedia).toHaveBeenCalled();
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
it('camera includes the mic by default', async () => {
|
|
188
|
-
const { result } = renderHook(() => useMediaRecorder({ source: 'camera' }));
|
|
189
|
-
await act(async () => {
|
|
190
|
-
await result.current.request();
|
|
191
|
-
});
|
|
192
|
-
const getUserMedia = navigator.mediaDevices.getUserMedia as ReturnType<typeof vi.fn>;
|
|
193
|
-
expect(getUserMedia).toHaveBeenCalledWith(
|
|
194
|
-
expect.objectContaining({ video: true, audio: true }),
|
|
195
|
-
);
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it('camera omits the mic when includeMicrophone is false', async () => {
|
|
199
|
-
const { result } = renderHook(() =>
|
|
200
|
-
useMediaRecorder({ source: 'camera', includeMicrophone: false }),
|
|
201
|
-
);
|
|
202
|
-
await act(async () => {
|
|
203
|
-
await result.current.request();
|
|
204
|
-
});
|
|
205
|
-
const getUserMedia = navigator.mediaDevices.getUserMedia as ReturnType<typeof vi.fn>;
|
|
206
|
-
expect(getUserMedia).toHaveBeenCalledWith(
|
|
207
|
-
expect.objectContaining({ video: true, audio: false }),
|
|
208
|
-
);
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
it('cancel() tears down state and stops the stream tracks', async () => {
|
|
212
|
-
const { result } = renderHook(() => useMediaRecorder({ source: 'mic' }));
|
|
213
|
-
|
|
214
|
-
await act(async () => {
|
|
215
|
-
await result.current.request();
|
|
216
|
-
});
|
|
217
|
-
const stream = result.current.stream as unknown as FakeMediaStream;
|
|
218
|
-
expect(stream).not.toBeNull();
|
|
219
|
-
const tracks = stream.getTracks();
|
|
220
|
-
|
|
221
|
-
act(() => {
|
|
222
|
-
result.current.cancel();
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
expect(result.current.state).toBe('idle');
|
|
226
|
-
expect(result.current.stream).toBeNull();
|
|
227
|
-
expect(tracks.every((t) => t.stop.mock.calls.length > 0)).toBe(true);
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
it('deduplicates concurrent permission requests', async () => {
|
|
231
|
-
let resolve!: (stream: FakeMediaStream) => void;
|
|
232
|
-
const pending = new Promise<FakeMediaStream>((res) => {
|
|
233
|
-
resolve = res;
|
|
234
|
-
});
|
|
235
|
-
const getUserMedia = vi.fn().mockReturnValue(pending);
|
|
236
|
-
Object.defineProperty(globalThis, 'navigator', {
|
|
237
|
-
configurable: true,
|
|
238
|
-
value: { mediaDevices: { getUserMedia } },
|
|
239
|
-
});
|
|
240
|
-
const stream = new FakeMediaStream([new FakeMediaStreamTrack('audio')]);
|
|
241
|
-
const { result } = renderHook(() => useMediaRecorder({ source: 'mic' }));
|
|
242
|
-
let first!: Promise<void>;
|
|
243
|
-
let second!: Promise<void>;
|
|
244
|
-
act(() => {
|
|
245
|
-
first = result.current.request();
|
|
246
|
-
second = result.current.request();
|
|
247
|
-
});
|
|
248
|
-
expect(getUserMedia).toHaveBeenCalledTimes(1);
|
|
249
|
-
resolve(stream);
|
|
250
|
-
await act(async () => Promise.all([first, second]));
|
|
251
|
-
expect(result.current.state).toBe('ready');
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
it('releases an acquired stream when recorder construction fails', async () => {
|
|
255
|
-
class ThrowingRecorder {
|
|
256
|
-
static isTypeSupported() {
|
|
257
|
-
return true;
|
|
258
|
-
}
|
|
259
|
-
constructor() {
|
|
260
|
-
throw new Error('construction failed');
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
(globalThis as { MediaRecorder?: unknown }).MediaRecorder = ThrowingRecorder;
|
|
264
|
-
const { result } = renderHook(() => useMediaRecorder({ source: 'mic' }));
|
|
265
|
-
await act(async () => {
|
|
266
|
-
await expect(result.current.request()).rejects.toThrow('construction failed');
|
|
267
|
-
});
|
|
268
|
-
expect(lastStream?.getTracks().every((track) => track.stop.mock.calls.length > 0)).toBe(true);
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
it('ignores a late onstop event after cancel', async () => {
|
|
272
|
-
const { result } = renderHook(() => useMediaRecorder({ source: 'mic' }));
|
|
273
|
-
await act(async () => result.current.request());
|
|
274
|
-
act(() => result.current.start());
|
|
275
|
-
const lateOnStop = lastRecorder?.onstop;
|
|
276
|
-
act(() => result.current.cancel());
|
|
277
|
-
act(() => lateOnStop?.());
|
|
278
|
-
expect(result.current.state).toBe('idle');
|
|
279
|
-
expect(result.current.blob).toBeNull();
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
it('returns the same in-flight stop promise to repeated callers', async () => {
|
|
283
|
-
const { result } = renderHook(() => useMediaRecorder({ source: 'mic' }));
|
|
284
|
-
await act(async () => result.current.request());
|
|
285
|
-
act(() => result.current.start());
|
|
286
|
-
const recorder = lastRecorder!;
|
|
287
|
-
const complete = recorder.onstop!;
|
|
288
|
-
recorder.stop = vi.fn(() => {
|
|
289
|
-
recorder.state = 'inactive';
|
|
290
|
-
});
|
|
291
|
-
let first!: Promise<Blob | null>;
|
|
292
|
-
let second!: Promise<Blob | null>;
|
|
293
|
-
act(() => {
|
|
294
|
-
first = result.current.stop();
|
|
295
|
-
second = result.current.stop();
|
|
296
|
-
});
|
|
297
|
-
expect(second).toBe(first);
|
|
298
|
-
act(() => complete());
|
|
299
|
-
await first;
|
|
300
|
-
expect(result.current.state).toBe('stopped');
|
|
301
|
-
});
|
|
302
|
-
});
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { advanceTime } from '../useTimelineClock';
|
|
3
|
-
|
|
4
|
-
describe('advanceTime', () => {
|
|
5
|
-
it('advances by dt within range', () => {
|
|
6
|
-
expect(advanceTime(2, 1.5, 30)).toBe(3.5);
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
it('clamps to total at the end', () => {
|
|
10
|
-
expect(advanceTime(29.5, 1, 30)).toBe(30);
|
|
11
|
-
expect(advanceTime(30, 5, 30)).toBe(30);
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it('never goes below 0', () => {
|
|
15
|
-
expect(advanceTime(0.2, -1, 30)).toBe(0);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it('returns 0 for an empty timeline', () => {
|
|
19
|
-
expect(advanceTime(5, 1, 0)).toBe(0);
|
|
20
|
-
});
|
|
21
|
-
});
|