@bendyline/squisq-editor-react 2.0.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (357) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +40 -0
  3. package/README.md +10 -2
  4. package/dist/index.d.ts +683 -60
  5. package/dist/index.js +9648 -3750
  6. package/dist/monaco.js +0 -1
  7. package/dist/styles/index.css +1160 -241
  8. package/package.json +10 -6
  9. package/dist/index.js.map +0 -1
  10. package/dist/monaco.js.map +0 -1
  11. package/src/BlockCardView.tsx +0 -121
  12. package/src/BlockPreviewPanel.tsx +0 -69
  13. package/src/BlockPropertiesPopover.tsx +0 -222
  14. package/src/DocumentSettingsDialog.tsx +0 -272
  15. package/src/DropZoneOverlay.tsx +0 -137
  16. package/src/EditorContext.tsx +0 -1247
  17. package/src/EditorShell.tsx +0 -1334
  18. package/src/EmojiPicker.tsx +0 -332
  19. package/src/FolderView.tsx +0 -131
  20. package/src/Icon.tsx +0 -26
  21. package/src/ImageEditor.tsx +0 -395
  22. package/src/ImageNodeView.tsx +0 -284
  23. package/src/ImageViewer.tsx +0 -221
  24. package/src/InlineIcon.ts +0 -84
  25. package/src/InlinePreviewGutter.tsx +0 -590
  26. package/src/LinkDialog.tsx +0 -276
  27. package/src/MediaBin.tsx +0 -500
  28. package/src/MentionExtension.tsx +0 -261
  29. package/src/OutlinePanel.tsx +0 -547
  30. package/src/PlainHtmlPreview.tsx +0 -277
  31. package/src/PreviewControls.tsx +0 -1250
  32. package/src/PreviewPanel.tsx +0 -422
  33. package/src/RawEditor.tsx +0 -731
  34. package/src/RecorderEntry.tsx +0 -159
  35. package/src/StatusBar.tsx +0 -51
  36. package/src/TemplateAnnotation.ts +0 -167
  37. package/src/TemplateContentPreview.tsx +0 -56
  38. package/src/TemplatePicker.tsx +0 -1314
  39. package/src/ThemeCustomizerPanel.tsx +0 -304
  40. package/src/ThemePicker.tsx +0 -433
  41. package/src/TimelineBlockPreview.tsx +0 -37
  42. package/src/TimelineTrack.tsx +0 -671
  43. package/src/Toolbar.tsx +0 -2587
  44. package/src/Tooltip.tsx +0 -142
  45. package/src/TransitionPicker.tsx +0 -390
  46. package/src/VersionHistoryPanel.tsx +0 -359
  47. package/src/ViewMenuPanel.tsx +0 -189
  48. package/src/ViewSwitcher.tsx +0 -56
  49. package/src/WysiwygEditor.tsx +0 -792
  50. package/src/__tests__/blockProperties.test.ts +0 -92
  51. package/src/__tests__/blockPropertiesPopoverTheme.test.tsx +0 -33
  52. package/src/__tests__/blockRange.test.ts +0 -105
  53. package/src/__tests__/blockTagActivity.test.ts +0 -183
  54. package/src/__tests__/buildDocumentPreviewMarkdown.test.ts +0 -75
  55. package/src/__tests__/buildPreviewDocContent.test.ts +0 -48
  56. package/src/__tests__/buildPreviewDocTransition.test.ts +0 -92
  57. package/src/__tests__/canvasSurfaceTextEditing.test.tsx +0 -60
  58. package/src/__tests__/codeContextSectionView.test.tsx +0 -97
  59. package/src/__tests__/codeContextZoneManager.test.ts +0 -127
  60. package/src/__tests__/createShapeLayer.test.ts +0 -46
  61. package/src/__tests__/customLayoutManagerTheme.test.tsx +0 -34
  62. package/src/__tests__/detectMarkdown.test.ts +0 -73
  63. package/src/__tests__/diffContextSections.test.ts +0 -39
  64. package/src/__tests__/documentSettingsDialog.test.tsx +0 -197
  65. package/src/__tests__/drawingShapeRoundTrip.test.ts +0 -49
  66. package/src/__tests__/editorScrollHandoff.test.tsx +0 -102
  67. package/src/__tests__/editorScrollSync.test.ts +0 -65
  68. package/src/__tests__/editorShellCodeContext.test.tsx +0 -86
  69. package/src/__tests__/editorShellProps.test.tsx +0 -485
  70. package/src/__tests__/embeddedMedia.test.ts +0 -48
  71. package/src/__tests__/emojiPicker.test.tsx +0 -93
  72. package/src/__tests__/fileKind.test.ts +0 -112
  73. package/src/__tests__/headingTransition.test.ts +0 -176
  74. package/src/__tests__/imageEditAffordance.test.tsx +0 -272
  75. package/src/__tests__/imageEditModalContract.test.tsx +0 -119
  76. package/src/__tests__/imageEditorShell.test.tsx +0 -94
  77. package/src/__tests__/imageEditorState.test.ts +0 -171
  78. package/src/__tests__/inlinePreviewGutter.test.tsx +0 -62
  79. package/src/__tests__/inlinePreviewGutterAllBlocks.test.tsx +0 -103
  80. package/src/__tests__/jsonEditor.test.tsx +0 -223
  81. package/src/__tests__/layersPanel.test.tsx +0 -167
  82. package/src/__tests__/layoutChildRoundTrip.test.ts +0 -71
  83. package/src/__tests__/linkDialogDocPicker.test.tsx +0 -137
  84. package/src/__tests__/majorApiTypes.test.ts +0 -26
  85. package/src/__tests__/mediaAttachmentFlow.test.ts +0 -110
  86. package/src/__tests__/mediaBinDrop.test.tsx +0 -90
  87. package/src/__tests__/mediaReferences.test.ts +0 -82
  88. package/src/__tests__/narrationSave.test.ts +0 -148
  89. package/src/__tests__/outlinePanel.test.tsx +0 -169
  90. package/src/__tests__/outlineSource.test.ts +0 -217
  91. package/src/__tests__/plainHtmlPreview.test.tsx +0 -125
  92. package/src/__tests__/presentationMode.test.tsx +0 -290
  93. package/src/__tests__/previewControls.test.tsx +0 -540
  94. package/src/__tests__/previewPanelAudioFailure.test.tsx +0 -46
  95. package/src/__tests__/previewPanelPresentation.test.tsx +0 -126
  96. package/src/__tests__/previewPanelTransforms.test.tsx +0 -117
  97. package/src/__tests__/propertiesPanel.test.tsx +0 -69
  98. package/src/__tests__/rawEditorModelIsolation.test.ts +0 -19
  99. package/src/__tests__/recorderFormats.test.ts +0 -146
  100. package/src/__tests__/recorderMediaInsert.test.ts +0 -86
  101. package/src/__tests__/recorderTheme.test.tsx +0 -42
  102. package/src/__tests__/recorderTimingJson.test.ts +0 -41
  103. package/src/__tests__/resolveBlockVisual.test.ts +0 -43
  104. package/src/__tests__/selectionConversions.test.ts +0 -80
  105. package/src/__tests__/statusBar.test.tsx +0 -27
  106. package/src/__tests__/teleprompter.test.tsx +0 -190
  107. package/src/__tests__/templateAnnotationRoundTrip.test.ts +0 -99
  108. package/src/__tests__/templateContentPreview.test.ts +0 -101
  109. package/src/__tests__/templatePickerMetadata.test.ts +0 -32
  110. package/src/__tests__/templatePickerPortal.test.tsx +0 -62
  111. package/src/__tests__/timelineSource.test.ts +0 -134
  112. package/src/__tests__/tiptapBridge.test.ts +0 -566
  113. package/src/__tests__/tiptapBridgeConformance.test.ts +0 -47
  114. package/src/__tests__/tiptapCodeBlockRoundTrip.test.ts +0 -109
  115. package/src/__tests__/tiptapImageRoundTrip.test.ts +0 -73
  116. package/src/__tests__/toolbarSelectionConversion.test.tsx +0 -164
  117. package/src/__tests__/tooltip.test.tsx +0 -72
  118. package/src/__tests__/transformStyleId.test.ts +0 -13
  119. package/src/__tests__/transitionCatalog.test.ts +0 -64
  120. package/src/__tests__/useBlockNavigator.test.tsx +0 -67
  121. package/src/__tests__/useImageEditor.test.tsx +0 -226
  122. package/src/__tests__/useJsonEditorTokens.test.ts +0 -59
  123. package/src/__tests__/useMediaRecorder.test.ts +0 -302
  124. package/src/__tests__/useTimelineClock.test.ts +0 -21
  125. package/src/__tests__/versionHistory.test.tsx +0 -197
  126. package/src/__tests__/viewMenuPanel.test.tsx +0 -130
  127. package/src/__tests__/wysiwygImageUpload.test.ts +0 -69
  128. package/src/__tests__/wysiwygTemplateBadgeFocus.test.tsx +0 -56
  129. package/src/asciiDiagram/AsciiDiagramExtension.ts +0 -343
  130. package/src/asciiDiagram/AsciiDiagramWidget.tsx +0 -254
  131. package/src/asciiDiagram/RepairableDiagramExtension.ts +0 -200
  132. package/src/asciiDiagram/__tests__/AsciiDiagramExtension.test.ts +0 -145
  133. package/src/asciiDiagram/__tests__/RepairableDiagramExtension.test.ts +0 -159
  134. package/src/asciiDiagram/__tests__/asciiDiagramCommands.test.ts +0 -284
  135. package/src/asciiDiagram/__tests__/asciiDiagramOps.test.ts +0 -114
  136. package/src/asciiDiagram/__tests__/asciiPaste.test.ts +0 -43
  137. package/src/asciiDiagram/asciiDiagramCommands.ts +0 -174
  138. package/src/asciiDiagram/asciiDiagramData.ts +0 -149
  139. package/src/asciiDiagram/asciiDiagramOps.ts +0 -238
  140. package/src/asciiDiagram/asciiPaste.ts +0 -19
  141. package/src/blockProperties.ts +0 -88
  142. package/src/blockRange.ts +0 -132
  143. package/src/blockSlice.ts +0 -75
  144. package/src/blockTagActivity.ts +0 -233
  145. package/src/buildDocumentPreviewMarkdown.ts +0 -168
  146. package/src/buildPreviewDoc.ts +0 -405
  147. package/src/codeContext/CodeContextSectionView.tsx +0 -124
  148. package/src/codeContext/CodeContextZoneManager.ts +0 -149
  149. package/src/codeContext/CodeContextZones.tsx +0 -121
  150. package/src/codeContext/diffContextSections.ts +0 -38
  151. package/src/codeContext/types.ts +0 -75
  152. package/src/customTemplates/AddBin.tsx +0 -126
  153. package/src/customTemplates/CustomLayoutManager.tsx +0 -234
  154. package/src/customTemplates/CustomTemplateContext.tsx +0 -188
  155. package/src/customTemplates/LayerToolbar.tsx +0 -580
  156. package/src/customTemplates/ShapeGlyph.tsx +0 -47
  157. package/src/customTemplates/TemplateDesigner.tsx +0 -433
  158. package/src/customTemplates/__tests__/library.test.ts +0 -96
  159. package/src/customTemplates/__tests__/normalizePositions.test.ts +0 -109
  160. package/src/customTemplates/__tests__/shapeDefs.test.ts +0 -49
  161. package/src/customTemplates/__tests__/useMemoryLayerAdapter.test.ts +0 -108
  162. package/src/customTemplates/designer.css +0 -732
  163. package/src/customTemplates/index.ts +0 -31
  164. package/src/customTemplates/library.ts +0 -110
  165. package/src/customTemplates/normalizePositions.ts +0 -75
  166. package/src/customTemplates/shapeDefs.ts +0 -131
  167. package/src/customTemplates/thumbnail.tsx +0 -63
  168. package/src/customTemplates/tokenDefs.ts +0 -60
  169. package/src/customTemplates/useDocCustomTemplates.ts +0 -52
  170. package/src/customTemplates/useMemoryLayerAdapter.ts +0 -129
  171. package/src/customThemes/CustomThemeContext.tsx +0 -185
  172. package/src/customThemes/CustomThemeDialog.tsx +0 -316
  173. package/src/customThemes/ImportThemeSection.tsx +0 -178
  174. package/src/customThemes/__tests__/CustomThemeContext.test.tsx +0 -64
  175. package/src/customThemes/__tests__/CustomThemeDialog.test.tsx +0 -47
  176. package/src/customThemes/__tests__/customThemeLibrary.test.ts +0 -60
  177. package/src/customThemes/__tests__/importThemeSection.test.tsx +0 -192
  178. package/src/customThemes/customThemeLibrary.ts +0 -100
  179. package/src/customThemes/index.ts +0 -37
  180. package/src/customThemes/themeControls.tsx +0 -229
  181. package/src/customThemes/themeDraft.ts +0 -288
  182. package/src/customThemes/useDocCustomThemes.ts +0 -49
  183. package/src/detectMarkdown.ts +0 -62
  184. package/src/diagram/DiagramCanvas.tsx +0 -259
  185. package/src/diagram/DiagramMaximizedOverlay.tsx +0 -46
  186. package/src/diagram/diagramCommands.ts +0 -604
  187. package/src/diagram/diagramConstants.ts +0 -17
  188. package/src/diagram/types.ts +0 -35
  189. package/src/editorScrollSync.ts +0 -73
  190. package/src/embeddedMedia.ts +0 -78
  191. package/src/emojiData.ts +0 -1317
  192. package/src/fileKind.ts +0 -158
  193. package/src/frontmatter.ts +0 -29
  194. package/src/frontmatterSettings.ts +0 -23
  195. package/src/headingTransition.ts +0 -291
  196. package/src/hooks/useFileDrop.ts +0 -266
  197. package/src/imageEditor/CanvasSurface.tsx +0 -703
  198. package/src/imageEditor/ImageVersionHistoryDropdown.tsx +0 -396
  199. package/src/imageEditor/LayersPanel.tsx +0 -218
  200. package/src/imageEditor/PropertiesPanel.tsx +0 -474
  201. package/src/imageEditor/Toolbar.tsx +0 -459
  202. package/src/imageEditor/createShapeLayer.ts +0 -280
  203. package/src/imageEditor/icons.tsx +0 -68
  204. package/src/imageEditor/image-editor.css +0 -574
  205. package/src/imageEditor/layers/EditorImageLayer.tsx +0 -45
  206. package/src/imageEditor/layers/EditorShapeLayer.tsx +0 -62
  207. package/src/imageEditor/layers/EditorTextLayer.tsx +0 -45
  208. package/src/imageEditor/layers/SelectionHandles.tsx +0 -86
  209. package/src/imageEditor/state.ts +0 -173
  210. package/src/imageEditor/useImageEditor.ts +0 -385
  211. package/src/imageEditor/useImageEditorTokens.ts +0 -70
  212. package/src/index.ts +0 -470
  213. package/src/jsonEditor/EmbeddedRichTextField.tsx +0 -81
  214. package/src/jsonEditor/JsonEditor.tsx +0 -81
  215. package/src/jsonEditor/JsonEditorContext.tsx +0 -81
  216. package/src/jsonEditor/RenderNode.tsx +0 -87
  217. package/src/jsonEditor/editors.tsx +0 -745
  218. package/src/jsonEditor/index.ts +0 -2
  219. package/src/jsonEditor/json-editor.css +0 -463
  220. package/src/jsonEditor/useJsonEditorTokens.ts +0 -33
  221. package/src/mediaDragMime.ts +0 -32
  222. package/src/mediaEntries.ts +0 -12
  223. package/src/mediaReferences.ts +0 -299
  224. package/src/monaco.ts +0 -35
  225. package/src/monacoWorkers.ts +0 -89
  226. package/src/outlineSource.ts +0 -171
  227. package/src/presentation/PresentationMode.tsx +0 -596
  228. package/src/rawEditorIsolation.ts +0 -18
  229. package/src/recorder/RecorderButton.tsx +0 -80
  230. package/src/recorder/RecorderModal.tsx +0 -706
  231. package/src/recorder/RecorderPanel.tsx +0 -77
  232. package/src/recorder/formats.ts +0 -159
  233. package/src/recorder/hooks/useMediaRecorder.ts +0 -436
  234. package/src/recorder/hooks/useStreamPreview.ts +0 -47
  235. package/src/recorder/insertMediaBlock.ts +0 -30
  236. package/src/recorder/sources/cameraStream.ts +0 -32
  237. package/src/recorder/sources/micStream.ts +0 -25
  238. package/src/recorder/sources/screenStream.ts +0 -162
  239. package/src/recorder/timingJson.ts +0 -66
  240. package/src/resolveBlockVisual.ts +0 -39
  241. package/src/scene/Scene.tsx +0 -616
  242. package/src/scene/SceneBlockExtension.ts +0 -205
  243. package/src/scene/SceneBlockToolbar.tsx +0 -201
  244. package/src/scene/SceneBlockWidget.tsx +0 -441
  245. package/src/scene/ScenePropsBar.tsx +0 -85
  246. package/src/scene/SceneSelection.tsx +0 -111
  247. package/src/scene/SceneSideToolbar.tsx +0 -89
  248. package/src/scene/SceneViewport.tsx +0 -106
  249. package/src/scene/ShapePalette.tsx +0 -196
  250. package/src/scene/__tests__/DiagramAdapter.test.ts +0 -142
  251. package/src/scene/__tests__/SceneBlockExtension.test.ts +0 -33
  252. package/src/scene/__tests__/bezierEdit.test.ts +0 -85
  253. package/src/scene/__tests__/blockLayers.test.ts +0 -57
  254. package/src/scene/__tests__/sceneIsolation.test.tsx +0 -119
  255. package/src/scene/__tests__/shapeLayers.test.ts +0 -106
  256. package/src/scene/__tests__/useSceneHitTest.test.ts +0 -90
  257. package/src/scene/__tests__/useScenePanZoom.test.ts +0 -103
  258. package/src/scene/adapters/DiagramAdapter.ts +0 -79
  259. package/src/scene/adapters/DrawingAdapter.ts +0 -420
  260. package/src/scene/adapters/LayoutAdapter.ts +0 -313
  261. package/src/scene/adapters/blockLayers.ts +0 -159
  262. package/src/scene/commands/SceneCommand.ts +0 -84
  263. package/src/scene/commands/drawingCommands.ts +0 -318
  264. package/src/scene/commands/layoutCommands.ts +0 -301
  265. package/src/scene/hooks/useSceneHitTest.ts +0 -105
  266. package/src/scene/hooks/useScenePanZoom.ts +0 -147
  267. package/src/scene/hooks/useSceneSelection.ts +0 -62
  268. package/src/scene/index.ts +0 -90
  269. package/src/scene/layers/DiagramEdges.tsx +0 -142
  270. package/src/scene/layers/edgeGeometry.ts +0 -104
  271. package/src/scene/layers/nodeCard.tsx +0 -164
  272. package/src/scene/layers/renderLayer.tsx +0 -70
  273. package/src/scene/layers/shapeLayers.ts +0 -201
  274. package/src/scene/paths/bezierEdit.ts +0 -208
  275. package/src/scene/scene.css +0 -792
  276. package/src/scene/text/SceneTextOverlay.tsx +0 -161
  277. package/src/scene/text/sceneTextChannel.ts +0 -46
  278. package/src/scene/text/sceneTextConfig.ts +0 -31
  279. package/src/scene/text/sceneTiptap.ts +0 -36
  280. package/src/scene/text/useSceneTextEditing.ts +0 -39
  281. package/src/scene/tools/ConnectTool.ts +0 -209
  282. package/src/scene/tools/DrawingConnectTool.ts +0 -231
  283. package/src/scene/tools/PathTool.ts +0 -158
  284. package/src/scene/tools/PlaceTool.ts +0 -47
  285. package/src/scene/tools/SceneTool.ts +0 -85
  286. package/src/scene/tools/SelectTool.ts +0 -300
  287. package/src/scene/tools/ShapeTool.ts +0 -143
  288. package/src/scene/tools/TextTool.ts +0 -72
  289. package/src/scene/tools/TokenTool.ts +0 -95
  290. package/src/scene/tools/createDrawShapeTool.ts +0 -82
  291. package/src/selectionConversions.ts +0 -155
  292. package/src/styles/ascii-diagram.css +0 -79
  293. package/src/styles/ascii-timeline.css +0 -596
  294. package/src/styles/code-context.css +0 -155
  295. package/src/styles/diagram.css +0 -183
  296. package/src/styles/editor.css +0 -6811
  297. package/src/styles/folder-view.css +0 -210
  298. package/src/styles/image-edit-affordance.css +0 -201
  299. package/src/styles/index.css +0 -18
  300. package/src/styles/tree-view.css +0 -189
  301. package/src/teleprompter/TeleprompterControls.tsx +0 -218
  302. package/src/teleprompter/TeleprompterSelfView.tsx +0 -22
  303. package/src/teleprompter/TeleprompterSurface.tsx +0 -267
  304. package/src/teleprompter/TeleprompterView.tsx +0 -338
  305. package/src/teleprompter/canvasRenderer.ts +0 -161
  306. package/src/teleprompter/floatingWindow.ts +0 -352
  307. package/src/teleprompter/index.ts +0 -61
  308. package/src/teleprompter/pcmWorklet.ts +0 -62
  309. package/src/teleprompter/recording/insertPreamble.ts +0 -80
  310. package/src/teleprompter/recording/narrationSave.ts +0 -134
  311. package/src/teleprompter/recording/useNarrationRecorder.ts +0 -367
  312. package/src/teleprompter/scrollModel.ts +0 -85
  313. package/src/teleprompter/teleprompterTheme.ts +0 -303
  314. package/src/teleprompter/types.ts +0 -38
  315. package/src/teleprompter/useFloatingWindow.ts +0 -74
  316. package/src/teleprompter/useMicAnalysis.ts +0 -211
  317. package/src/teleprompter/useTeleprompter.ts +0 -421
  318. package/src/templateContentPreviewResolver.ts +0 -360
  319. package/src/timeline/TimelineEditorWidget.tsx +0 -861
  320. package/src/timeline/TimelineViewExtension.ts +0 -252
  321. package/src/timeline/__tests__/TimelineEditorWidget.test.tsx +0 -414
  322. package/src/timeline/__tests__/TimelineViewExtension.test.ts +0 -163
  323. package/src/timeline/__tests__/timelineCommands.test.ts +0 -359
  324. package/src/timeline/__tests__/timelineOps.test.ts +0 -277
  325. package/src/timeline/__tests__/timelinePaste.test.ts +0 -34
  326. package/src/timeline/timelineCommands.ts +0 -421
  327. package/src/timeline/timelineData.ts +0 -54
  328. package/src/timeline/timelineOps.ts +0 -381
  329. package/src/timeline/timelinePaste.ts +0 -8
  330. package/src/timelineSource.ts +0 -244
  331. package/src/tiptap/TiptapAudio.tsx +0 -86
  332. package/src/tiptap/TiptapVideo.tsx +0 -119
  333. package/src/tiptap/useResolvedMediaSrc.ts +0 -47
  334. package/src/tiptapBridge.ts +0 -986
  335. package/src/tooltipPlacement.ts +0 -13
  336. package/src/transformStyleId.ts +0 -17
  337. package/src/transitionCatalog.ts +0 -159
  338. package/src/treeview/TreeOutlineWidget.tsx +0 -390
  339. package/src/treeview/TreeViewExtension.ts +0 -250
  340. package/src/treeview/__tests__/TreeOutlineWidget.test.tsx +0 -156
  341. package/src/treeview/__tests__/TreeViewExtension.test.ts +0 -122
  342. package/src/treeview/__tests__/treeOps.test.ts +0 -177
  343. package/src/treeview/__tests__/treePaste.test.ts +0 -40
  344. package/src/treeview/__tests__/treeViewCommands.test.ts +0 -179
  345. package/src/treeview/treeOps.ts +0 -231
  346. package/src/treeview/treePaste.ts +0 -13
  347. package/src/treeview/treeViewCommands.ts +0 -99
  348. package/src/treeview/treeViewData.ts +0 -41
  349. package/src/types/monaco-shims.d.ts +0 -10
  350. package/src/useBlockNavigator.ts +0 -153
  351. package/src/useHeadingLayout.ts +0 -294
  352. package/src/useMonacoLoader.ts +0 -82
  353. package/src/useTimelineClock.ts +0 -76
  354. package/src/utils/collectInlineFontAwesomeCss.ts +0 -69
  355. package/src/utils/dropUtils.ts +0 -136
  356. package/src/utils/normalizeMalformedAssetUrl.ts +0 -22
  357. 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
- });