@bendyline/squisq-editor-react 1.6.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -3
- package/dist/index.d.ts +1304 -208
- package/dist/index.js +22838 -14494
- package/dist/index.js.map +1 -1
- package/dist/monaco.d.ts +1 -0
- package/dist/monaco.js +4 -0
- package/dist/monaco.js.map +1 -0
- package/dist/styles/index.css +2384 -408
- package/package.json +9 -5
- package/src/BlockPropertiesPopover.tsx +48 -17
- package/src/DocumentSettingsDialog.tsx +13 -21
- package/src/EditorContext.tsx +199 -42
- package/src/EditorShell.tsx +403 -270
- package/src/ImageEditor.tsx +22 -1
- package/src/InlinePreviewGutter.tsx +16 -8
- package/src/MediaBin.tsx +276 -40
- package/src/OutlinePanel.tsx +202 -7
- package/src/PlainHtmlPreview.tsx +40 -1
- package/src/PreviewControls.tsx +466 -93
- package/src/PreviewPanel.tsx +313 -77
- package/src/RawEditor.tsx +13 -2
- package/src/StatusBar.tsx +5 -2
- package/src/TemplateAnnotation.ts +34 -10
- package/src/TemplateContentPreview.tsx +56 -0
- package/src/TemplatePicker.tsx +355 -134
- package/src/ThemeCustomizerPanel.tsx +30 -15
- package/src/ThemePicker.tsx +10 -5
- package/src/TimelineBlockPreview.tsx +1 -1
- package/src/Toolbar.tsx +719 -238
- package/src/TransitionPicker.tsx +44 -5
- package/src/ViewMenuPanel.tsx +12 -14
- package/src/ViewSwitcher.tsx +4 -4
- package/src/WysiwygEditor.tsx +167 -92
- package/src/__tests__/blockPropertiesPopoverTheme.test.tsx +33 -0
- package/src/__tests__/blockTagActivity.test.ts +183 -0
- package/src/__tests__/buildDocumentPreviewMarkdown.test.ts +75 -0
- package/src/__tests__/buildPreviewDocContent.test.ts +48 -0
- package/src/__tests__/buildPreviewDocTransition.test.ts +22 -3
- package/src/__tests__/canvasSurfaceTextEditing.test.tsx +60 -0
- package/src/__tests__/customLayoutManagerTheme.test.tsx +34 -0
- package/src/__tests__/documentSettingsDialog.test.tsx +29 -1
- package/src/__tests__/editorScrollHandoff.test.tsx +102 -0
- package/src/__tests__/editorScrollSync.test.ts +65 -0
- package/src/__tests__/editorShellProps.test.tsx +390 -1
- package/src/__tests__/emojiPicker.test.tsx +2 -42
- package/src/__tests__/headingTransition.test.ts +53 -15
- package/src/__tests__/imageEditAffordance.test.tsx +5 -1
- package/src/__tests__/imageEditModalContract.test.tsx +119 -0
- package/src/__tests__/imageEditorShell.test.tsx +38 -1
- package/src/__tests__/jsonEditor.test.tsx +56 -1
- package/src/__tests__/layersPanel.test.tsx +75 -5
- package/src/__tests__/majorApiTypes.test.ts +26 -0
- package/src/__tests__/mediaBinDrop.test.tsx +90 -0
- package/src/__tests__/mediaReferences.test.ts +82 -0
- package/src/__tests__/narrationSave.test.ts +148 -0
- package/src/__tests__/outlinePanel.test.tsx +93 -3
- package/src/__tests__/outlineSource.test.ts +217 -0
- package/src/__tests__/plainHtmlPreview.test.tsx +17 -1
- package/src/__tests__/presentationMode.test.tsx +290 -0
- package/src/__tests__/previewControls.test.tsx +312 -6
- package/src/__tests__/previewPanelAudioFailure.test.tsx +46 -0
- package/src/__tests__/previewPanelPresentation.test.tsx +126 -0
- package/src/__tests__/previewPanelTransforms.test.tsx +117 -0
- package/src/__tests__/rawEditorModelIsolation.test.ts +19 -0
- package/src/__tests__/resolveBlockVisual.test.ts +43 -0
- package/src/__tests__/statusBar.test.tsx +27 -0
- package/src/__tests__/teleprompter.test.tsx +190 -0
- package/src/__tests__/templateAnnotationRoundTrip.test.ts +49 -2
- package/src/__tests__/templateContentPreview.test.ts +101 -0
- package/src/__tests__/templatePickerPortal.test.tsx +62 -0
- package/src/__tests__/tiptapBridge.test.ts +70 -0
- package/src/__tests__/tiptapCodeBlockRoundTrip.test.ts +109 -0
- package/src/__tests__/transformStyleId.test.ts +13 -0
- package/src/__tests__/useImageEditor.test.tsx +67 -0
- package/src/__tests__/useMediaRecorder.test.ts +75 -0
- package/src/__tests__/viewMenuPanel.test.tsx +130 -0
- package/src/__tests__/wysiwygImageUpload.test.ts +69 -0
- package/src/__tests__/wysiwygTemplateBadgeFocus.test.tsx +56 -0
- package/src/asciiDiagram/AsciiDiagramExtension.ts +343 -0
- package/src/asciiDiagram/AsciiDiagramWidget.tsx +224 -0
- package/src/asciiDiagram/RepairableDiagramExtension.ts +200 -0
- package/src/asciiDiagram/__tests__/AsciiDiagramExtension.test.ts +145 -0
- package/src/asciiDiagram/__tests__/RepairableDiagramExtension.test.ts +159 -0
- package/src/asciiDiagram/__tests__/asciiDiagramCommands.test.ts +227 -0
- package/src/asciiDiagram/__tests__/asciiDiagramOps.test.ts +114 -0
- package/src/asciiDiagram/__tests__/asciiPaste.test.ts +43 -0
- package/src/asciiDiagram/asciiDiagramCommands.ts +148 -0
- package/src/asciiDiagram/asciiDiagramData.ts +116 -0
- package/src/asciiDiagram/asciiDiagramOps.ts +219 -0
- package/src/asciiDiagram/asciiPaste.ts +19 -0
- package/src/blockTagActivity.ts +233 -0
- package/src/buildDocumentPreviewMarkdown.ts +168 -0
- package/src/buildPreviewDoc.ts +19 -12
- package/src/customTemplates/CustomLayoutManager.tsx +3 -2
- package/src/customTemplates/CustomTemplateContext.tsx +6 -0
- package/src/customTemplates/TemplateDesigner.tsx +7 -4
- package/src/customTemplates/__tests__/library.test.ts +8 -0
- package/src/customTemplates/designer.css +158 -99
- package/src/customTemplates/library.ts +16 -3
- package/src/customThemes/CustomThemeContext.tsx +6 -0
- package/src/customThemes/CustomThemeDialog.tsx +35 -5
- package/src/customThemes/ImportThemeSection.tsx +178 -0
- package/src/customThemes/__tests__/customThemeLibrary.test.ts +9 -0
- package/src/customThemes/__tests__/importThemeSection.test.tsx +192 -0
- package/src/customThemes/customThemeLibrary.ts +7 -4
- package/src/customThemes/index.ts +7 -1
- package/src/customThemes/themeDraft.ts +23 -7
- package/src/diagram/DiagramCanvas.tsx +9 -4
- package/src/diagram/types.ts +35 -0
- package/src/editorScrollSync.ts +73 -0
- package/src/emojiData.ts +3 -23
- package/src/headingTransition.ts +85 -25
- package/src/imageEditor/CanvasSurface.tsx +29 -23
- package/src/imageEditor/LayersPanel.tsx +78 -3
- package/src/imageEditor/Toolbar.tsx +5 -1
- package/src/imageEditor/icons.tsx +4 -0
- package/src/imageEditor/image-editor.css +75 -0
- package/src/imageEditor/layers/SelectionHandles.tsx +1 -1
- package/src/imageEditor/useImageEditor.ts +71 -14
- package/src/index.ts +193 -20
- package/src/jsonEditor/JsonEditorContext.tsx +12 -6
- package/src/jsonEditor/RenderNode.tsx +24 -3
- package/src/jsonEditor/editors.tsx +86 -19
- package/src/mediaEntries.ts +12 -0
- package/src/mediaReferences.ts +299 -0
- package/src/monaco.ts +35 -0
- package/src/monacoWorkers.ts +89 -0
- package/src/outlineSource.ts +171 -0
- package/src/presentation/PresentationMode.tsx +596 -0
- package/src/rawEditorIsolation.ts +18 -0
- package/src/recorder/hooks/useMediaRecorder.ts +97 -53
- package/src/resolveBlockVisual.ts +10 -4
- package/src/scene/Scene.tsx +101 -25
- package/src/scene/SceneBlockExtension.ts +17 -10
- package/src/scene/SceneBlockWidget.tsx +11 -4
- package/src/scene/SceneSelection.tsx +19 -15
- package/src/scene/SceneSideToolbar.tsx +89 -0
- package/src/scene/SceneViewport.tsx +7 -3
- package/src/scene/ShapePalette.tsx +17 -2
- package/src/scene/__tests__/DiagramAdapter.test.ts +86 -0
- package/src/scene/__tests__/SceneBlockExtension.test.ts +33 -0
- package/src/scene/__tests__/sceneIsolation.test.tsx +93 -0
- package/src/scene/adapters/DiagramAdapter.ts +12 -101
- package/src/scene/commands/SceneCommand.ts +4 -1
- package/src/scene/index.ts +1 -6
- package/src/scene/layers/DiagramEdges.tsx +24 -9
- package/src/scene/layers/edgeGeometry.ts +31 -4
- package/src/scene/layers/nodeCard.tsx +27 -8
- package/src/scene/scene.css +145 -2
- package/src/scene/text/SceneTextOverlay.tsx +5 -5
- package/src/scene/text/sceneTextChannel.ts +26 -20
- package/src/scene/text/sceneTextConfig.ts +4 -0
- package/src/scene/tools/ConnectTool.ts +126 -28
- package/src/scene/tools/DrawingConnectTool.ts +86 -16
- package/src/scene/tools/SceneTool.ts +10 -0
- package/src/scene/tools/SelectTool.ts +34 -18
- package/src/scene/tools/ShapeTool.ts +2 -3
- package/src/styles/ascii-diagram.css +79 -0
- package/src/styles/ascii-timeline.css +499 -0
- package/src/styles/editor.css +1566 -219
- package/src/styles/index.css +3 -0
- package/src/styles/tree-view.css +139 -0
- package/src/teleprompter/TeleprompterControls.tsx +218 -0
- package/src/teleprompter/TeleprompterSelfView.tsx +22 -0
- package/src/teleprompter/TeleprompterSurface.tsx +267 -0
- package/src/teleprompter/TeleprompterView.tsx +338 -0
- package/src/teleprompter/canvasRenderer.ts +161 -0
- package/src/teleprompter/floatingWindow.ts +352 -0
- package/src/teleprompter/index.ts +61 -0
- package/src/teleprompter/pcmWorklet.ts +62 -0
- package/src/teleprompter/recording/insertPreamble.ts +80 -0
- package/src/teleprompter/recording/narrationSave.ts +134 -0
- package/src/teleprompter/recording/useNarrationRecorder.ts +367 -0
- package/src/teleprompter/scrollModel.ts +85 -0
- package/src/teleprompter/teleprompterTheme.ts +303 -0
- package/src/teleprompter/types.ts +38 -0
- package/src/teleprompter/useFloatingWindow.ts +74 -0
- package/src/teleprompter/useMicAnalysis.ts +211 -0
- package/src/teleprompter/useTeleprompter.ts +421 -0
- package/src/templateContentPreviewResolver.ts +360 -0
- package/src/timeline/TimelineEditorWidget.tsx +702 -0
- package/src/timeline/TimelineViewExtension.ts +252 -0
- package/src/timeline/__tests__/TimelineEditorWidget.test.tsx +355 -0
- package/src/timeline/__tests__/TimelineViewExtension.test.ts +163 -0
- package/src/timeline/__tests__/timelineCommands.test.ts +327 -0
- package/src/timeline/__tests__/timelineOps.test.ts +222 -0
- package/src/timeline/__tests__/timelinePaste.test.ts +34 -0
- package/src/timeline/timelineCommands.ts +367 -0
- package/src/timeline/timelineData.ts +54 -0
- package/src/timeline/timelineOps.ts +277 -0
- package/src/timeline/timelinePaste.ts +8 -0
- package/src/tiptapBridge.ts +84 -49
- package/src/transformStyleId.ts +17 -0
- package/src/treeview/TreeOutlineWidget.tsx +240 -0
- package/src/treeview/TreeViewExtension.ts +250 -0
- package/src/treeview/__tests__/TreeViewExtension.test.ts +122 -0
- package/src/treeview/__tests__/treeOps.test.ts +125 -0
- package/src/treeview/__tests__/treePaste.test.ts +40 -0
- package/src/treeview/__tests__/treeViewCommands.test.ts +163 -0
- package/src/treeview/treeOps.ts +172 -0
- package/src/treeview/treePaste.ts +13 -0
- package/src/treeview/treeViewCommands.ts +94 -0
- package/src/treeview/treeViewData.ts +41 -0
- package/src/useMonacoLoader.ts +11 -34
- package/src/wysiwygImageUpload.ts +113 -0
- package/src/diagram/DiagramExtension.ts +0 -209
- package/src/diagram/DiagramWidget.tsx +0 -270
- package/src/diagram/useDiagramData.ts +0 -126
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/** @vitest-environment jsdom */
|
|
2
|
+
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import type { ContentContainer } from '@bendyline/squisq/storage';
|
|
5
|
+
import { MemoryContentContainer } from '@bendyline/squisq/storage';
|
|
6
|
+
import type { MediaProvider } from '@bendyline/squisq/schemas';
|
|
7
|
+
import { useEditorContext } from '../EditorContext';
|
|
8
|
+
|
|
9
|
+
const imageEditorProps: Array<{
|
|
10
|
+
filesContainer: ContentContainer;
|
|
11
|
+
saveFormat: 'png' | 'jpeg' | 'webp';
|
|
12
|
+
onExport: (blob: Blob, format: 'png' | 'jpeg' | 'webp') => void;
|
|
13
|
+
}> = [];
|
|
14
|
+
|
|
15
|
+
vi.mock('../ImageEditor', () => ({
|
|
16
|
+
ImageEditor: (props: (typeof imageEditorProps)[number]) => {
|
|
17
|
+
imageEditorProps.push(props);
|
|
18
|
+
return <div data-testid="image-editor-contract-stub" />;
|
|
19
|
+
},
|
|
20
|
+
}));
|
|
21
|
+
vi.mock('../RawEditor', () => ({ RawEditor: () => <div /> }));
|
|
22
|
+
vi.mock('../WysiwygEditor', () => ({ WysiwygEditor: () => <div /> }));
|
|
23
|
+
vi.mock('../PreviewPanel', () => ({ PreviewPanel: () => <div /> }));
|
|
24
|
+
|
|
25
|
+
import { EditorShell } from '../EditorShell';
|
|
26
|
+
|
|
27
|
+
function Trigger({ path }: { path: string }) {
|
|
28
|
+
const { openImageEdit } = useEditorContext();
|
|
29
|
+
return <button onClick={() => openImageEdit(path)}>edit</button>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function provider(): MediaProvider {
|
|
33
|
+
return {
|
|
34
|
+
addMedia: vi.fn(async (name: string) => name),
|
|
35
|
+
resolveUrl: vi.fn(async (name: string) => `blob:${name}`),
|
|
36
|
+
listMedia: vi.fn(async () => []),
|
|
37
|
+
removeMedia: vi.fn(async () => {}),
|
|
38
|
+
dispose: vi.fn(),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
imageEditorProps.length = 0;
|
|
44
|
+
if (typeof window.matchMedia !== 'function') {
|
|
45
|
+
Object.defineProperty(window, 'matchMedia', {
|
|
46
|
+
configurable: true,
|
|
47
|
+
value: () => ({ matches: false, addEventListener() {}, removeEventListener() {} }),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('ImageEditModal storage contract', () => {
|
|
53
|
+
it('writes JPEG bytes/MIME back to a .jpg path', async () => {
|
|
54
|
+
const mediaProvider = provider();
|
|
55
|
+
render(
|
|
56
|
+
<EditorShell
|
|
57
|
+
initialMarkdown="# image"
|
|
58
|
+
mediaProvider={mediaProvider}
|
|
59
|
+
toolbarSlotRight={<Trigger path="photo.jpg" />}
|
|
60
|
+
/>,
|
|
61
|
+
);
|
|
62
|
+
fireEvent.click(screen.getByText('edit'));
|
|
63
|
+
await waitFor(() => expect(screen.getByTestId('image-editor-contract-stub')).toBeTruthy());
|
|
64
|
+
const props = imageEditorProps[imageEditorProps.length - 1]!;
|
|
65
|
+
expect(props.saveFormat).toBe('jpeg');
|
|
66
|
+
await act(async () => props.onExport(new Blob(['jpeg']), 'jpeg'));
|
|
67
|
+
expect(mediaProvider.addMedia).toHaveBeenCalledWith(
|
|
68
|
+
'photo.jpg',
|
|
69
|
+
expect.any(Blob),
|
|
70
|
+
'image/jpeg',
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('hashes sidecar paths so sanitized-name collisions remain separate', async () => {
|
|
75
|
+
const parent = new MemoryContentContainer();
|
|
76
|
+
const mediaProvider = provider();
|
|
77
|
+
const first = render(
|
|
78
|
+
<EditorShell
|
|
79
|
+
initialMarkdown="# image"
|
|
80
|
+
workspaceContainer={parent}
|
|
81
|
+
mediaProvider={mediaProvider}
|
|
82
|
+
toolbarSlotRight={<Trigger path="a/b.png" />}
|
|
83
|
+
/>,
|
|
84
|
+
);
|
|
85
|
+
fireEvent.click(screen.getByText('edit'));
|
|
86
|
+
await waitFor(() => expect(imageEditorProps).toHaveLength(1));
|
|
87
|
+
const firstContainer = imageEditorProps[0].filesContainer;
|
|
88
|
+
first.unmount();
|
|
89
|
+
|
|
90
|
+
render(
|
|
91
|
+
<EditorShell
|
|
92
|
+
initialMarkdown="# image"
|
|
93
|
+
workspaceContainer={parent}
|
|
94
|
+
mediaProvider={mediaProvider}
|
|
95
|
+
toolbarSlotRight={<Trigger path="a_b.png" />}
|
|
96
|
+
/>,
|
|
97
|
+
);
|
|
98
|
+
fireEvent.click(screen.getByText('edit'));
|
|
99
|
+
await waitFor(() => expect(imageEditorProps).toHaveLength(2));
|
|
100
|
+
const secondContainer = imageEditorProps[1].filesContainer;
|
|
101
|
+
await firstContainer.writeFile('sentinel', new Uint8Array([1]).buffer);
|
|
102
|
+
await secondContainer.writeFile('sentinel', new Uint8Array([2]).buffer);
|
|
103
|
+
const paths = (await parent.listFiles('.imageEdits')).map((entry) => entry.path);
|
|
104
|
+
expect(paths.filter((path) => path.endsWith('/sentinel'))).toHaveLength(2);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('refuses unsupported source formats instead of writing mismatched bytes', async () => {
|
|
108
|
+
render(
|
|
109
|
+
<EditorShell
|
|
110
|
+
initialMarkdown="# image"
|
|
111
|
+
mediaProvider={provider()}
|
|
112
|
+
toolbarSlotRight={<Trigger path="animated.gif" />}
|
|
113
|
+
/>,
|
|
114
|
+
);
|
|
115
|
+
fireEvent.click(screen.getByText('edit'));
|
|
116
|
+
await waitFor(() => expect(screen.getByText(/supports PNG, JPEG, and WebP/i)).toBeTruthy());
|
|
117
|
+
expect(screen.queryByTestId('image-editor-contract-stub')).toBeNull();
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
* renders the toolbar / canvas / panels.
|
|
7
7
|
*/
|
|
8
8
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
9
|
-
import { render, screen, waitFor } from '@testing-library/react';
|
|
9
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
10
10
|
import { MemoryContentContainer, scopeContainer } from '@bendyline/squisq/storage';
|
|
11
11
|
import { writeImageEditDoc, createEmptyImageEditDoc } from '@bendyline/squisq/imageEdit';
|
|
12
|
+
import { DARK_SURFACE } from '@bendyline/squisq/schemas';
|
|
12
13
|
import { ImageEditor } from '../ImageEditor.js';
|
|
13
14
|
|
|
14
15
|
beforeEach(() => {
|
|
@@ -54,4 +55,40 @@ describe('<ImageEditor>', () => {
|
|
|
54
55
|
render(<ImageEditor filesContainer={sidecar} />);
|
|
55
56
|
expect(screen.getByText(/loading image editor/i)).toBeTruthy();
|
|
56
57
|
});
|
|
58
|
+
|
|
59
|
+
it('adds a centered shape layer from the Layers panel menu', async () => {
|
|
60
|
+
const parent = new MemoryContentContainer();
|
|
61
|
+
const sidecar = scopeContainer(parent, 'pic_files');
|
|
62
|
+
await writeImageEditDoc(sidecar, createEmptyImageEditDoc(640, 480));
|
|
63
|
+
|
|
64
|
+
render(<ImageEditor filesContainer={sidecar} />);
|
|
65
|
+
await waitFor(() => expect(screen.getByTestId('image-editor')).toBeTruthy());
|
|
66
|
+
|
|
67
|
+
fireEvent.click(screen.getByRole('button', { name: 'Add layer' }));
|
|
68
|
+
fireEvent.click(screen.getByRole('menuitem', { name: 'Shape layer' }));
|
|
69
|
+
|
|
70
|
+
await waitFor(() => expect(screen.getByTitle('Rectangle')).toBeTruthy());
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('applies an explicit dark surface to the editor chrome', async () => {
|
|
74
|
+
const parent = new MemoryContentContainer();
|
|
75
|
+
const sidecar = scopeContainer(parent, 'pic_files');
|
|
76
|
+
await writeImageEditDoc(sidecar, createEmptyImageEditDoc(64, 48));
|
|
77
|
+
|
|
78
|
+
render(<ImageEditor filesContainer={sidecar} surface={DARK_SURFACE} />);
|
|
79
|
+
|
|
80
|
+
await waitFor(() => {
|
|
81
|
+
expect(screen.getByTestId('image-editor')).toBeTruthy();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const editor = screen.getByTestId('image-editor');
|
|
85
|
+
expect(editor.style.getPropertyValue('--squisq-image-editor-bg')).toBe(DARK_SURFACE.background);
|
|
86
|
+
expect(editor.style.getPropertyValue('--squisq-image-editor-text')).toBe(DARK_SURFACE.text);
|
|
87
|
+
expect(editor.style.getPropertyValue('--squisq-image-editor-panel-bg')).toBe(
|
|
88
|
+
`color-mix(in srgb, ${DARK_SURFACE.background} 92%, ${DARK_SURFACE.text} 8%)`,
|
|
89
|
+
);
|
|
90
|
+
expect(editor.style.getPropertyValue('--squisq-image-editor-control-bg')).toBe(
|
|
91
|
+
`color-mix(in srgb, ${DARK_SURFACE.background} 86%, ${DARK_SURFACE.text} 14%)`,
|
|
92
|
+
);
|
|
93
|
+
});
|
|
57
94
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from 'vitest';
|
|
2
2
|
import { useState } from 'react';
|
|
3
|
-
import { fireEvent, render, screen } from '@testing-library/react';
|
|
3
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
4
4
|
import type { SquisqAnnotatedSchema } from '@bendyline/squisq/jsonForm';
|
|
5
5
|
import { JsonEditor } from '../jsonEditor';
|
|
6
6
|
|
|
@@ -165,4 +165,59 @@ describe('JsonEditor', () => {
|
|
|
165
165
|
fireEvent.change(input, { target: { value: 'changed' } });
|
|
166
166
|
expect(onChange).not.toHaveBeenCalled();
|
|
167
167
|
});
|
|
168
|
+
|
|
169
|
+
it('escapes object property names as RFC 6901 pointer segments', () => {
|
|
170
|
+
const schema: SquisqAnnotatedSchema = {
|
|
171
|
+
type: 'object',
|
|
172
|
+
properties: { 'a/b~c': { type: 'string', title: 'Odd key' } },
|
|
173
|
+
};
|
|
174
|
+
const onChange = vi.fn();
|
|
175
|
+
render(<Controlled schema={schema} initial={{ 'a/b~c': 'before' }} onValueChange={onChange} />);
|
|
176
|
+
fireEvent.change(screen.getByLabelText('Odd key'), { target: { value: 'after' } });
|
|
177
|
+
expect(onChange).toHaveBeenLastCalledWith({ 'a/b~c': 'after' });
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('associates labels/help/errors and reports validation after render', async () => {
|
|
181
|
+
const schema: SquisqAnnotatedSchema = {
|
|
182
|
+
type: 'object',
|
|
183
|
+
properties: {
|
|
184
|
+
title: { type: 'string', title: 'Title', description: 'A useful title' },
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
const onValidate = vi.fn();
|
|
188
|
+
render(
|
|
189
|
+
<JsonEditor
|
|
190
|
+
schema={schema}
|
|
191
|
+
value={{ title: '' }}
|
|
192
|
+
onChange={() => {}}
|
|
193
|
+
validate={() => [{ path: '/title', message: 'Required' }]}
|
|
194
|
+
onValidate={onValidate}
|
|
195
|
+
/>,
|
|
196
|
+
);
|
|
197
|
+
const input = screen.getByLabelText('Title');
|
|
198
|
+
expect(input.getAttribute('aria-invalid')).toBe('true');
|
|
199
|
+
const described = input.getAttribute('aria-describedby') ?? '';
|
|
200
|
+
expect(described).toContain('-help');
|
|
201
|
+
expect(described).toContain('-error');
|
|
202
|
+
expect(screen.getByRole('alert').textContent).toBe('Required');
|
|
203
|
+
await waitFor(() =>
|
|
204
|
+
expect(onValidate).toHaveBeenCalledWith([{ path: '/title', message: 'Required' }]),
|
|
205
|
+
);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('adds ARIA state and keyboard navigation to segmented/tabs controls', () => {
|
|
209
|
+
const schema: SquisqAnnotatedSchema = {
|
|
210
|
+
oneOf: [
|
|
211
|
+
{ type: 'string', title: 'Text option' },
|
|
212
|
+
{ type: 'number', title: 'Number option' },
|
|
213
|
+
],
|
|
214
|
+
squisq: { control: 'tabs', label: 'Value type' },
|
|
215
|
+
};
|
|
216
|
+
render(<JsonEditor schema={schema} value="hello" onChange={() => {}} />);
|
|
217
|
+
const tabs = screen.getAllByRole('tab');
|
|
218
|
+
expect(tabs[0].getAttribute('aria-selected')).toBe('true');
|
|
219
|
+
fireEvent.keyDown(tabs[0], { key: 'ArrowRight' });
|
|
220
|
+
expect(tabs[1].getAttribute('aria-selected')).toBe('true');
|
|
221
|
+
expect(screen.getByRole('tabpanel')).toBeTruthy();
|
|
222
|
+
});
|
|
168
223
|
});
|
|
@@ -7,6 +7,8 @@ import type { ImageEditDoc } from '@bendyline/squisq/schemas';
|
|
|
7
7
|
import { LayersPanel } from '../imageEditor/LayersPanel.js';
|
|
8
8
|
import type { ImageEditorAction } from '../imageEditor/state.js';
|
|
9
9
|
|
|
10
|
+
const ignoreAddLayer = () => {};
|
|
11
|
+
|
|
10
12
|
function buildDoc(): ImageEditDoc {
|
|
11
13
|
return {
|
|
12
14
|
version: 1,
|
|
@@ -41,7 +43,14 @@ function buildDoc(): ImageEditDoc {
|
|
|
41
43
|
describe('LayersPanel', () => {
|
|
42
44
|
it('renders layers with the top of the SVG stack first', () => {
|
|
43
45
|
const dispatch = vi.fn<(a: ImageEditorAction) => void>();
|
|
44
|
-
render(
|
|
46
|
+
render(
|
|
47
|
+
<LayersPanel
|
|
48
|
+
doc={buildDoc()}
|
|
49
|
+
selectedLayerId={null}
|
|
50
|
+
dispatch={dispatch}
|
|
51
|
+
onAddLayer={ignoreAddLayer}
|
|
52
|
+
/>,
|
|
53
|
+
);
|
|
45
54
|
const items = screen.getAllByText(/Bottom|Middle|Top/);
|
|
46
55
|
expect(items[0]?.textContent).toContain('Top');
|
|
47
56
|
expect(items[items.length - 1]?.textContent).toContain('Bottom');
|
|
@@ -49,14 +58,60 @@ describe('LayersPanel', () => {
|
|
|
49
58
|
|
|
50
59
|
it('clicking the layer name dispatches a select action', () => {
|
|
51
60
|
const dispatch = vi.fn<(a: ImageEditorAction) => void>();
|
|
52
|
-
render(
|
|
61
|
+
render(
|
|
62
|
+
<LayersPanel
|
|
63
|
+
doc={buildDoc()}
|
|
64
|
+
selectedLayerId={null}
|
|
65
|
+
dispatch={dispatch}
|
|
66
|
+
onAddLayer={ignoreAddLayer}
|
|
67
|
+
/>,
|
|
68
|
+
);
|
|
53
69
|
fireEvent.click(screen.getByText('Middle'));
|
|
54
70
|
expect(dispatch).toHaveBeenCalledWith({ type: 'select', layerId: 'b' });
|
|
55
71
|
});
|
|
56
72
|
|
|
73
|
+
it('exposes the full layer name as a tooltip when the row is truncated', () => {
|
|
74
|
+
const dispatch = vi.fn<(a: ImageEditorAction) => void>();
|
|
75
|
+
render(
|
|
76
|
+
<LayersPanel
|
|
77
|
+
doc={buildDoc()}
|
|
78
|
+
selectedLayerId={null}
|
|
79
|
+
dispatch={dispatch}
|
|
80
|
+
onAddLayer={ignoreAddLayer}
|
|
81
|
+
/>,
|
|
82
|
+
);
|
|
83
|
+
expect(screen.getByTitle('Middle').textContent).toContain('Middle');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('opens the add-layer menu and reports the selected layer kind', () => {
|
|
87
|
+
const dispatch = vi.fn<(a: ImageEditorAction) => void>();
|
|
88
|
+
const onAddLayer = vi.fn();
|
|
89
|
+
render(
|
|
90
|
+
<LayersPanel
|
|
91
|
+
doc={buildDoc()}
|
|
92
|
+
selectedLayerId={null}
|
|
93
|
+
dispatch={dispatch}
|
|
94
|
+
onAddLayer={onAddLayer}
|
|
95
|
+
/>,
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
fireEvent.click(screen.getByRole('button', { name: 'Add layer' }));
|
|
99
|
+
expect(screen.getByRole('menu')).toBeTruthy();
|
|
100
|
+
fireEvent.click(screen.getByRole('menuitem', { name: 'Text layer' }));
|
|
101
|
+
expect(onAddLayer).toHaveBeenCalledWith('text');
|
|
102
|
+
expect(screen.queryByRole('menu')).toBeNull();
|
|
103
|
+
});
|
|
104
|
+
|
|
57
105
|
it('toggling visibility dispatches an update-layer action', () => {
|
|
58
106
|
const dispatch = vi.fn<(a: ImageEditorAction) => void>();
|
|
59
|
-
render(
|
|
107
|
+
render(
|
|
108
|
+
<LayersPanel
|
|
109
|
+
doc={buildDoc()}
|
|
110
|
+
selectedLayerId={null}
|
|
111
|
+
dispatch={dispatch}
|
|
112
|
+
onAddLayer={ignoreAddLayer}
|
|
113
|
+
/>,
|
|
114
|
+
);
|
|
60
115
|
// The "Hide layer" buttons exist for visible layers.
|
|
61
116
|
const hideButtons = screen.getAllByRole('button', { name: 'Hide layer' });
|
|
62
117
|
fireEvent.click(hideButtons[0]!); // top-most visible layer is 'b'
|
|
@@ -69,7 +124,14 @@ describe('LayersPanel', () => {
|
|
|
69
124
|
|
|
70
125
|
it('move-up button is disabled at the top of the stack', () => {
|
|
71
126
|
const dispatch = vi.fn<(a: ImageEditorAction) => void>();
|
|
72
|
-
render(
|
|
127
|
+
render(
|
|
128
|
+
<LayersPanel
|
|
129
|
+
doc={buildDoc()}
|
|
130
|
+
selectedLayerId={null}
|
|
131
|
+
dispatch={dispatch}
|
|
132
|
+
onAddLayer={ignoreAddLayer}
|
|
133
|
+
/>,
|
|
134
|
+
);
|
|
73
135
|
const upButtons = screen.getAllByRole('button', { name: 'Move layer up' });
|
|
74
136
|
// Visual order is c, b, a; layer 'c' is at the top of the stack and cannot move up.
|
|
75
137
|
expect((upButtons[0] as HTMLButtonElement).disabled).toBe(true);
|
|
@@ -77,7 +139,14 @@ describe('LayersPanel', () => {
|
|
|
77
139
|
|
|
78
140
|
it('delete button dispatches a remove-layer action', () => {
|
|
79
141
|
const dispatch = vi.fn<(a: ImageEditorAction) => void>();
|
|
80
|
-
render(
|
|
142
|
+
render(
|
|
143
|
+
<LayersPanel
|
|
144
|
+
doc={buildDoc()}
|
|
145
|
+
selectedLayerId={null}
|
|
146
|
+
dispatch={dispatch}
|
|
147
|
+
onAddLayer={ignoreAddLayer}
|
|
148
|
+
/>,
|
|
149
|
+
);
|
|
81
150
|
const delButtons = screen.getAllByRole('button', { name: 'Delete layer' });
|
|
82
151
|
fireEvent.click(delButtons[0]!); // top of stack ('c')
|
|
83
152
|
expect(dispatch).toHaveBeenCalledWith({ type: 'remove-layer', layerId: 'c' });
|
|
@@ -90,6 +159,7 @@ describe('LayersPanel', () => {
|
|
|
90
159
|
doc={{ version: 1, canvas: { width: 1, height: 1 }, layers: [] }}
|
|
91
160
|
selectedLayerId={null}
|
|
92
161
|
dispatch={dispatch}
|
|
162
|
+
onAddLayer={ignoreAddLayer}
|
|
93
163
|
/>,
|
|
94
164
|
);
|
|
95
165
|
expect(screen.getByText(/no layers yet/i)).toBeTruthy();
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { DiagramEdge, DiagramNode, EditorShellProps } from '../index';
|
|
3
|
+
|
|
4
|
+
// @ts-expect-error React Flow-prefixed types were removed in the major API cleanup.
|
|
5
|
+
import type { DiagramRFNode } from '../index';
|
|
6
|
+
|
|
7
|
+
describe('major-version editor API types', () => {
|
|
8
|
+
it('uses implementation-neutral diagram names', () => {
|
|
9
|
+
const node: DiagramNode = { id: 'a', position: { x: 0, y: 0 }, data: { label: 'A' } };
|
|
10
|
+
const edge: DiagramEdge = { id: 'a-b', source: 'a', target: 'b' };
|
|
11
|
+
expect([node.id, edge.id]).toEqual(['a', 'a-b']);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('does not accept the removed EditorShell container prop', () => {
|
|
15
|
+
const props: EditorShellProps = {
|
|
16
|
+
// @ts-expect-error Use workspaceContainer; container was removed.
|
|
17
|
+
container: null,
|
|
18
|
+
};
|
|
19
|
+
expect(props).toHaveProperty('container');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('keeps the old type import absent', () => {
|
|
23
|
+
const legacy: DiagramRFNode | null = null;
|
|
24
|
+
expect(legacy).toBeNull();
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
6
|
+
import type { MediaEntry, MediaProvider } from '@bendyline/squisq/schemas';
|
|
7
|
+
import { MediaBin } from '../MediaBin.js';
|
|
8
|
+
|
|
9
|
+
function createProvider(): { provider: MediaProvider; added: string[] } {
|
|
10
|
+
const entries: MediaEntry[] = [];
|
|
11
|
+
const added: string[] = [];
|
|
12
|
+
return {
|
|
13
|
+
added,
|
|
14
|
+
provider: {
|
|
15
|
+
async addMedia(name, data, mimeType) {
|
|
16
|
+
const relativePath = `attachments/${name}`;
|
|
17
|
+
added.push(relativePath);
|
|
18
|
+
entries.push({
|
|
19
|
+
name: relativePath,
|
|
20
|
+
mimeType,
|
|
21
|
+
size: data instanceof Blob ? data.size : data.byteLength,
|
|
22
|
+
});
|
|
23
|
+
return relativePath;
|
|
24
|
+
},
|
|
25
|
+
async resolveUrl(path) {
|
|
26
|
+
return `blob:${path}`;
|
|
27
|
+
},
|
|
28
|
+
async listMedia() {
|
|
29
|
+
return [...entries];
|
|
30
|
+
},
|
|
31
|
+
async removeMedia() {},
|
|
32
|
+
dispose() {},
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function dataTransfer(files: File[], itemTypes: string[]) {
|
|
38
|
+
return {
|
|
39
|
+
files,
|
|
40
|
+
items: itemTypes.map((type) => ({ kind: 'file', type })),
|
|
41
|
+
types: ['Files'],
|
|
42
|
+
dropEffect: 'none',
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe('MediaBin image drop target', () => {
|
|
47
|
+
it('shows a drop affordance and uploads a dropped image through the normal callback', async () => {
|
|
48
|
+
const { provider, added } = createProvider();
|
|
49
|
+
const onMediaUploaded = vi.fn();
|
|
50
|
+
const image = new File([new Uint8Array([1, 2, 3])], 'photo.png', { type: 'image/png' });
|
|
51
|
+
Object.defineProperty(image, 'arrayBuffer', {
|
|
52
|
+
configurable: true,
|
|
53
|
+
value: async () => new Uint8Array([1, 2, 3]).buffer,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const { container } = render(
|
|
57
|
+
<MediaBin mediaProvider={provider} isDark onMediaUploaded={onMediaUploaded} />,
|
|
58
|
+
);
|
|
59
|
+
const bin = container.querySelector('.squisq-media-bin') as HTMLElement;
|
|
60
|
+
const transfer = dataTransfer([image], ['image/png']);
|
|
61
|
+
await screen.findByText('No files yet.');
|
|
62
|
+
|
|
63
|
+
fireEvent.dragEnter(bin, { dataTransfer: transfer });
|
|
64
|
+
expect(bin.classList.contains('squisq-media-bin--drop-active')).toBe(true);
|
|
65
|
+
expect(screen.getByText('Drop images here')).toBeTruthy();
|
|
66
|
+
|
|
67
|
+
fireEvent.drop(bin, { dataTransfer: transfer });
|
|
68
|
+
|
|
69
|
+
await waitFor(() => expect(added).toEqual(['attachments/photo.png']));
|
|
70
|
+
expect(onMediaUploaded).toHaveBeenCalledWith('attachments/photo.png', 'photo.png', 'image/png');
|
|
71
|
+
expect(await screen.findByText('photo.png')).toBeTruthy();
|
|
72
|
+
expect(screen.queryByText('Drop images here')).toBeNull();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('does not activate or upload for a non-image file', async () => {
|
|
76
|
+
const { provider, added } = createProvider();
|
|
77
|
+
const text = new File(['notes'], 'notes.txt', { type: 'text/plain' });
|
|
78
|
+
const { container } = render(<MediaBin mediaProvider={provider} isDark={false} />);
|
|
79
|
+
const bin = container.querySelector('.squisq-media-bin') as HTMLElement;
|
|
80
|
+
const transfer = dataTransfer([text], ['text/plain']);
|
|
81
|
+
await screen.findByText('No files yet.');
|
|
82
|
+
|
|
83
|
+
fireEvent.dragEnter(bin, { dataTransfer: transfer });
|
|
84
|
+
fireEvent.drop(bin, { dataTransfer: transfer });
|
|
85
|
+
|
|
86
|
+
expect(bin.classList.contains('squisq-media-bin--drop-active')).toBe(false);
|
|
87
|
+
expect(screen.queryByText('Drop images here')).toBeNull();
|
|
88
|
+
await waitFor(() => expect(added).toEqual([]));
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
collectMediaReferencesFromMarkdown,
|
|
4
|
+
removeMediaReferencesFromMarkdown,
|
|
5
|
+
} from '../mediaReferences';
|
|
6
|
+
|
|
7
|
+
describe('collectMediaReferencesFromMarkdown', () => {
|
|
8
|
+
it('collects markdown image/link and html media references', () => {
|
|
9
|
+
const refs = collectMediaReferencesFromMarkdown(
|
|
10
|
+
[
|
|
11
|
+
'',
|
|
12
|
+
'[Two](attachments/two.pdf)',
|
|
13
|
+
'<video src="video/clip.webm" poster="video/poster.png"></video>',
|
|
14
|
+
'<a href="attachments/three.txt">Three</a>',
|
|
15
|
+
].join('\n'),
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
expect([...refs].sort()).toEqual([
|
|
19
|
+
'attachments/one.png',
|
|
20
|
+
'attachments/three.txt',
|
|
21
|
+
'attachments/two.pdf',
|
|
22
|
+
'video/clip.webm',
|
|
23
|
+
'video/poster.png',
|
|
24
|
+
]);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('collects media references from squiggly annotations', () => {
|
|
28
|
+
const refs = collectMediaReferencesFromMarkdown(
|
|
29
|
+
[
|
|
30
|
+
'## Intro {[audio=audio/take.mp3]}',
|
|
31
|
+
'{[video src="video/clip with spaces.webm"]}',
|
|
32
|
+
'### Image {#hero} {[image src=images/hero.png alt="Hero"]}',
|
|
33
|
+
].join('\n'),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
expect([...refs].sort()).toEqual([
|
|
37
|
+
'audio/take.mp3',
|
|
38
|
+
'images/hero.png',
|
|
39
|
+
'video/clip with spaces.webm',
|
|
40
|
+
]);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('removeMediaReferencesFromMarkdown', () => {
|
|
45
|
+
it('removes standalone image references to the media path', () => {
|
|
46
|
+
const source = ['Before', '', '', '', 'After'].join('\n');
|
|
47
|
+
|
|
48
|
+
expect(removeMediaReferencesFromMarkdown(source, 'attachments/pasted.png')).toBe(
|
|
49
|
+
['Before', '', '', 'After'].join('\n'),
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('removes inline image and link references without touching similar paths', () => {
|
|
54
|
+
const source = [
|
|
55
|
+
'Keep .',
|
|
56
|
+
'Drop  and [file](attachments/pasted.png).',
|
|
57
|
+
].join('\n');
|
|
58
|
+
|
|
59
|
+
expect(removeMediaReferencesFromMarkdown(source, 'attachments/pasted.png')).toBe(
|
|
60
|
+
['Keep .', 'Drop and .'].join('\n'),
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('supports angle-wrapped destinations and markdown titles', () => {
|
|
65
|
+
const source =
|
|
66
|
+
'Drop  and [A](attachments/simple.png "title").';
|
|
67
|
+
|
|
68
|
+
expect(removeMediaReferencesFromMarkdown(source, 'attachments/my pasted image.png')).toBe(
|
|
69
|
+
'Drop and [A](attachments/simple.png "title").',
|
|
70
|
+
);
|
|
71
|
+
expect(removeMediaReferencesFromMarkdown(source, 'attachments/simple.png')).toBe(
|
|
72
|
+
'Drop  and .',
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('removes resized raw HTML image tags and raw HTML anchors', () => {
|
|
77
|
+
const source =
|
|
78
|
+
'<img alt="Screenshot" src="attachments/pasted.png" width="320">\n<a href="attachments/pasted.png">Screenshot</a>';
|
|
79
|
+
|
|
80
|
+
expect(removeMediaReferencesFromMarkdown(source, 'attachments/pasted.png')).toBe('\n');
|
|
81
|
+
});
|
|
82
|
+
});
|