@bendyline/squisq-editor-react 1.6.2 → 2.0.1
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 +1275 -183
- package/dist/index.js +20345 -12777
- 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 +1607 -241
- package/package.json +9 -5
- package/src/BlockPropertiesPopover.tsx +30 -15
- package/src/DocumentSettingsDialog.tsx +39 -33
- package/src/EditorContext.tsx +199 -42
- package/src/EditorShell.tsx +349 -261
- package/src/ImageEditor.tsx +22 -1
- package/src/InlinePreviewGutter.tsx +16 -8
- package/src/MediaBin.tsx +107 -14
- package/src/OutlinePanel.tsx +202 -7
- package/src/PlainHtmlPreview.tsx +40 -1
- package/src/PreviewControls.tsx +362 -89
- package/src/PreviewPanel.tsx +312 -78
- package/src/RawEditor.tsx +13 -2
- package/src/RecorderEntry.tsx +2 -0
- package/src/StatusBar.tsx +5 -2
- package/src/TemplateAnnotation.ts +12 -3
- package/src/TemplatePicker.tsx +75 -21
- package/src/ThemeCustomizerPanel.tsx +8 -0
- package/src/ThemePicker.tsx +10 -5
- package/src/TimelineBlockPreview.tsx +1 -1
- package/src/Toolbar.tsx +357 -51
- package/src/TransitionPicker.tsx +36 -4
- package/src/ViewMenuPanel.tsx +12 -14
- package/src/WysiwygEditor.tsx +124 -91
- 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 +21 -1
- package/src/__tests__/canvasSurfaceTextEditing.test.tsx +60 -0
- package/src/__tests__/codeContextSectionView.test.tsx +8 -6
- package/src/__tests__/customLayoutManagerTheme.test.tsx +34 -0
- package/src/__tests__/documentSettingsDialog.test.tsx +51 -1
- package/src/__tests__/editorScrollHandoff.test.tsx +102 -0
- package/src/__tests__/editorScrollSync.test.ts +65 -0
- package/src/__tests__/editorShellProps.test.tsx +123 -1
- package/src/__tests__/emojiPicker.test.tsx +2 -42
- package/src/__tests__/headingTransition.test.ts +0 -12
- package/src/__tests__/imageEditAffordance.test.tsx +5 -1
- package/src/__tests__/imageEditModalContract.test.tsx +119 -0
- package/src/__tests__/imageEditorShell.test.tsx +15 -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__/mediaAttachmentFlow.test.ts +2 -2
- package/src/__tests__/mediaBinDrop.test.tsx +90 -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 +382 -5
- 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__/recorderTheme.test.tsx +42 -0
- package/src/__tests__/resolveBlockVisual.test.ts +43 -0
- package/src/__tests__/selectionConversions.test.ts +80 -0
- package/src/__tests__/statusBar.test.tsx +27 -0
- package/src/__tests__/teleprompter.test.tsx +190 -0
- package/src/__tests__/templateAnnotationRoundTrip.test.ts +26 -0
- package/src/__tests__/templatePickerPortal.test.tsx +62 -0
- package/src/__tests__/tiptapBridge.test.ts +71 -7
- package/src/__tests__/tiptapCodeBlockRoundTrip.test.ts +109 -0
- package/src/__tests__/tiptapImageRoundTrip.test.ts +1 -1
- package/src/__tests__/toolbarSelectionConversion.test.tsx +164 -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 +254 -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 +284 -0
- package/src/asciiDiagram/__tests__/asciiDiagramOps.test.ts +114 -0
- package/src/asciiDiagram/__tests__/asciiPaste.test.ts +43 -0
- package/src/asciiDiagram/asciiDiagramCommands.ts +174 -0
- package/src/asciiDiagram/asciiDiagramData.ts +149 -0
- package/src/asciiDiagram/asciiDiagramOps.ts +238 -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/codeContext/types.ts +1 -1
- 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/__tests__/useMemoryLayerAdapter.test.ts +13 -0
- package/src/customTemplates/designer.css +158 -99
- package/src/customTemplates/library.ts +16 -3
- package/src/customTemplates/useMemoryLayerAdapter.ts +7 -1
- 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 +25 -6
- package/src/diagram/types.ts +35 -0
- package/src/editorScrollSync.ts +73 -0
- package/src/emojiData.ts +3 -23
- package/src/frontmatterSettings.ts +23 -0
- package/src/headingTransition.ts +0 -15
- 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 +198 -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/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/RecorderButton.tsx +9 -1
- package/src/recorder/RecorderModal.tsx +84 -41
- package/src/recorder/RecorderPanel.tsx +9 -1
- package/src/recorder/hooks/useMediaRecorder.ts +97 -53
- package/src/resolveBlockVisual.ts +10 -4
- package/src/scene/Scene.tsx +48 -18
- package/src/scene/SceneBlockExtension.ts +17 -10
- package/src/scene/SceneBlockWidget.tsx +5 -1
- 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 +119 -0
- package/src/scene/adapters/DiagramAdapter.ts +12 -101
- package/src/scene/adapters/DrawingAdapter.ts +6 -1
- package/src/scene/adapters/LayoutAdapter.ts +3 -0
- package/src/scene/commands/SceneCommand.ts +17 -3
- package/src/scene/index.ts +1 -6
- package/src/scene/layers/DiagramEdges.tsx +21 -7
- package/src/scene/layers/edgeGeometry.ts +9 -1
- package/src/scene/layers/nodeCard.tsx +27 -8
- package/src/scene/scene.css +5 -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 +13 -4
- package/src/scene/tools/SceneTool.ts +8 -0
- package/src/scene/tools/SelectTool.ts +39 -23
- package/src/scene/tools/ShapeTool.ts +2 -3
- package/src/selectionConversions.ts +155 -0
- package/src/styles/ascii-diagram.css +79 -0
- package/src/styles/ascii-timeline.css +596 -0
- package/src/styles/editor.css +736 -120
- package/src/styles/index.css +3 -0
- package/src/styles/tree-view.css +189 -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 +12 -5
- package/src/timeline/TimelineEditorWidget.tsx +861 -0
- package/src/timeline/TimelineViewExtension.ts +252 -0
- package/src/timeline/__tests__/TimelineEditorWidget.test.tsx +414 -0
- package/src/timeline/__tests__/TimelineViewExtension.test.ts +163 -0
- package/src/timeline/__tests__/timelineCommands.test.ts +359 -0
- package/src/timeline/__tests__/timelineOps.test.ts +277 -0
- package/src/timeline/__tests__/timelinePaste.test.ts +34 -0
- package/src/timeline/timelineCommands.ts +421 -0
- package/src/timeline/timelineData.ts +54 -0
- package/src/timeline/timelineOps.ts +381 -0
- package/src/timeline/timelinePaste.ts +8 -0
- package/src/tiptapBridge.ts +47 -21
- package/src/transformStyleId.ts +17 -0
- package/src/treeview/TreeOutlineWidget.tsx +390 -0
- package/src/treeview/TreeViewExtension.ts +250 -0
- package/src/treeview/__tests__/TreeOutlineWidget.test.tsx +156 -0
- package/src/treeview/__tests__/TreeViewExtension.test.ts +122 -0
- package/src/treeview/__tests__/treeOps.test.ts +177 -0
- package/src/treeview/__tests__/treePaste.test.ts +40 -0
- package/src/treeview/__tests__/treeViewCommands.test.ts +179 -0
- package/src/treeview/treeOps.ts +231 -0
- package/src/treeview/treePaste.ts +13 -0
- package/src/treeview/treeViewCommands.ts +99 -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 -272
- package/src/diagram/useDiagramData.ts +0 -126
|
@@ -13,9 +13,10 @@
|
|
|
13
13
|
* the props it receives so we can assert `monacoTheme` reaches it.
|
|
14
14
|
*/
|
|
15
15
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
16
|
-
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
16
|
+
import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
|
|
17
17
|
import type { RawEditorProps } from '../RawEditor';
|
|
18
18
|
import type { MediaEntry, MediaProvider } from '@bendyline/squisq/schemas';
|
|
19
|
+
import { useMediaProvider } from '@bendyline/squisq-react';
|
|
19
20
|
|
|
20
21
|
// Records the props the shell passes to RawEditor on each render.
|
|
21
22
|
const rawEditorProps: RawEditorProps[] = [];
|
|
@@ -42,6 +43,13 @@ function MarkdownSourceProbe() {
|
|
|
42
43
|
return <pre data-testid="markdown-source">{markdownSource}</pre>;
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
function MediaProviderProbe({ expected }: { expected: MediaProvider }) {
|
|
47
|
+
const provider = useMediaProvider();
|
|
48
|
+
return (
|
|
49
|
+
<span data-testid="media-context-probe">{provider === expected ? 'same' : 'missing'}</span>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
45
53
|
function mediaProviderWith(count: number): MediaProvider {
|
|
46
54
|
const entries: MediaEntry[] = Array.from({ length: count }, (_, i) => ({
|
|
47
55
|
name: `file-${i + 1}.png`,
|
|
@@ -184,6 +192,61 @@ describe('RawEditor monacoTheme prop', () => {
|
|
|
184
192
|
});
|
|
185
193
|
});
|
|
186
194
|
|
|
195
|
+
describe('<EditorShell> instance boundaries', () => {
|
|
196
|
+
it('provides its mediaProvider to React preview/layer consumers', async () => {
|
|
197
|
+
const provider = mediaProviderWith(0);
|
|
198
|
+
await act(async () => {
|
|
199
|
+
render(
|
|
200
|
+
<EditorShell
|
|
201
|
+
initialMarkdown="# hi"
|
|
202
|
+
initialView="raw"
|
|
203
|
+
mediaProvider={provider}
|
|
204
|
+
toolbarSlotLeft={<MediaProviderProbe expected={provider} />}
|
|
205
|
+
/>,
|
|
206
|
+
);
|
|
207
|
+
});
|
|
208
|
+
expect(screen.getByTestId('media-context-probe').textContent).toBe('same');
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('keeps view shortcuts inside the editor that received the key', async () => {
|
|
212
|
+
const { container } = render(
|
|
213
|
+
<>
|
|
214
|
+
<EditorShell initialMarkdown="# first" initialView="raw" />
|
|
215
|
+
<EditorShell initialMarkdown="# second" initialView="raw" />
|
|
216
|
+
</>,
|
|
217
|
+
);
|
|
218
|
+
const shells = container.querySelectorAll<HTMLElement>('.squisq-editor-shell');
|
|
219
|
+
fireEvent.keyDown(within(shells[1]).getByTestId('raw-editor-stub'), {
|
|
220
|
+
key: '1',
|
|
221
|
+
ctrlKey: true,
|
|
222
|
+
});
|
|
223
|
+
await waitFor(() => expect(within(shells[1]).getByTestId('wysiwyg-editor-stub')).toBeTruthy());
|
|
224
|
+
expect(within(shells[0]).getByTestId('raw-editor-stub')).toBeTruthy();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('ignores file drops in readOnly mode', async () => {
|
|
228
|
+
const { container } = render(
|
|
229
|
+
<EditorShell
|
|
230
|
+
initialMarkdown="# original"
|
|
231
|
+
initialView="raw"
|
|
232
|
+
readOnly
|
|
233
|
+
toolbarSlotRight={<MarkdownSourceProbe />}
|
|
234
|
+
/>,
|
|
235
|
+
);
|
|
236
|
+
const shell = container.querySelector<HTMLElement>('.squisq-editor-shell')!;
|
|
237
|
+
const file = new File(['# replacement'], 'replacement.md', { type: 'text/markdown' });
|
|
238
|
+
fireEvent.drop(shell, {
|
|
239
|
+
dataTransfer: {
|
|
240
|
+
types: ['Files'],
|
|
241
|
+
files: [file],
|
|
242
|
+
items: [{ kind: 'file', type: 'text/markdown', getAsFile: () => file }],
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
await act(async () => Promise.resolve());
|
|
246
|
+
expect(screen.getByTestId('markdown-source').textContent).toBe('# original');
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
187
250
|
describe('<EditorShell> Files badge', () => {
|
|
188
251
|
it('shows the mediaProvider file count on the paperclip button', async () => {
|
|
189
252
|
const { container } = render(
|
|
@@ -231,6 +294,25 @@ describe('<EditorShell> Files badge', () => {
|
|
|
231
294
|
expect(container.querySelector('.squisq-toolbar-files-badge')?.textContent).toBe('1');
|
|
232
295
|
});
|
|
233
296
|
|
|
297
|
+
it('hides .gitignore entries from the Files panel and its count', async () => {
|
|
298
|
+
const { provider } = mutableMediaProviderWith([
|
|
299
|
+
{ name: '.gitignore', mimeType: 'text/plain', size: 11 },
|
|
300
|
+
{ name: 'notes_files/.gitignore', mimeType: 'text/plain', size: 11 },
|
|
301
|
+
{ name: 'notes_files/hero.png', mimeType: 'image/png', size: 1024 },
|
|
302
|
+
]);
|
|
303
|
+
|
|
304
|
+
render(<EditorShell initialMarkdown="# hi" initialView="raw" mediaProvider={provider} />);
|
|
305
|
+
|
|
306
|
+
await waitFor(() => {
|
|
307
|
+
expect(screen.getByLabelText('Toggle Files panel, 1 file')).toBeTruthy();
|
|
308
|
+
});
|
|
309
|
+
fireEvent.click(screen.getByLabelText('Toggle Files panel, 1 file'));
|
|
310
|
+
|
|
311
|
+
expect(await screen.findByText('hero.png')).toBeTruthy();
|
|
312
|
+
expect(screen.queryByText('.gitignore')).toBeNull();
|
|
313
|
+
expect(screen.getByText('Files (1)')).toBeTruthy();
|
|
314
|
+
});
|
|
315
|
+
|
|
234
316
|
it('removes a media file and matching markdown refs from the files context menu', async () => {
|
|
235
317
|
const { provider, removed } = mutableMediaProviderWith([
|
|
236
318
|
{
|
|
@@ -338,6 +420,28 @@ describe('<Toolbar> Files badge', () => {
|
|
|
338
420
|
});
|
|
339
421
|
expect(container.querySelector('.squisq-toolbar-files-badge')?.textContent).toBe('2');
|
|
340
422
|
});
|
|
423
|
+
|
|
424
|
+
it('excludes .gitignore from a self-scanned count', async () => {
|
|
425
|
+
const { provider } = mutableMediaProviderWith([
|
|
426
|
+
{ name: '.gitignore', mimeType: 'text/plain', size: 11 },
|
|
427
|
+
{ name: 'hero.png', mimeType: 'image/png', size: 1024 },
|
|
428
|
+
]);
|
|
429
|
+
const { container } = render(
|
|
430
|
+
<EditorProvider
|
|
431
|
+
initialMarkdown="# hi"
|
|
432
|
+
initialView="raw"
|
|
433
|
+
mediaProvider={provider}
|
|
434
|
+
allowRecording={false}
|
|
435
|
+
>
|
|
436
|
+
<Toolbar onToggleFiles={() => {}} />
|
|
437
|
+
</EditorProvider>,
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
await waitFor(() => {
|
|
441
|
+
expect(screen.getByLabelText('Toggle Files panel, 1 file')).toBeTruthy();
|
|
442
|
+
});
|
|
443
|
+
expect(container.querySelector('.squisq-toolbar-files-badge')?.textContent).toBe('1');
|
|
444
|
+
});
|
|
341
445
|
});
|
|
342
446
|
|
|
343
447
|
describe('<Toolbar> Insert menu', () => {
|
|
@@ -360,4 +464,22 @@ describe('<Toolbar> Insert menu', () => {
|
|
|
360
464
|
);
|
|
361
465
|
});
|
|
362
466
|
});
|
|
467
|
+
|
|
468
|
+
it('adds an explicit authored timeline fence from the Insert menu', async () => {
|
|
469
|
+
render(
|
|
470
|
+
<EditorProvider initialMarkdown="Intro" initialView="raw" allowRecording={false}>
|
|
471
|
+
<Toolbar />
|
|
472
|
+
<MarkdownSourceProbe />
|
|
473
|
+
</EditorProvider>,
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
fireEvent.click(screen.getByLabelText('Insert'));
|
|
477
|
+
fireEvent.click(await screen.findByRole('menuitem', { name: /Timeline/i }));
|
|
478
|
+
|
|
479
|
+
await waitFor(() => {
|
|
480
|
+
expect(screen.getByTestId('markdown-source').textContent).toContain(
|
|
481
|
+
'```timeline\nMilestones: ● Start',
|
|
482
|
+
);
|
|
483
|
+
});
|
|
484
|
+
});
|
|
363
485
|
});
|
|
@@ -1,43 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import { fireEvent, render, screen } from '@testing-library/react';
|
|
3
|
-
import { EmojiPicker } from '../
|
|
4
|
-
import {
|
|
5
|
-
EMOJI_CATEGORIES,
|
|
6
|
-
ALL_EMOJIS,
|
|
7
|
-
searchEmojis,
|
|
8
|
-
PICKER_CATEGORIES,
|
|
9
|
-
ALL_PICKER_ENTRIES,
|
|
10
|
-
searchPickerEntries,
|
|
11
|
-
} from '../emojiData';
|
|
12
|
-
|
|
13
|
-
describe('emojiData — legacy emoji exports', () => {
|
|
14
|
-
it('exposes the standard CLDR category buckets', () => {
|
|
15
|
-
const ids = EMOJI_CATEGORIES.map((c) => c.id);
|
|
16
|
-
expect(ids).toEqual([
|
|
17
|
-
'smileys',
|
|
18
|
-
'people',
|
|
19
|
-
'nature',
|
|
20
|
-
'food',
|
|
21
|
-
'travel',
|
|
22
|
-
'activities',
|
|
23
|
-
'objects',
|
|
24
|
-
'symbols',
|
|
25
|
-
'flags',
|
|
26
|
-
]);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('each entry has a non-empty char and name', () => {
|
|
30
|
-
for (const entry of ALL_EMOJIS) {
|
|
31
|
-
expect(entry.char.length).toBeGreaterThan(0);
|
|
32
|
-
expect(entry.name.length).toBeGreaterThan(0);
|
|
33
|
-
}
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('searchEmojis matches by name (case-insensitive)', () => {
|
|
37
|
-
const results = searchEmojis('HEART');
|
|
38
|
-
expect(results.some((e) => e.char === '❤️')).toBe(true);
|
|
39
|
-
});
|
|
40
|
-
});
|
|
3
|
+
import { EmojiPicker, PICKER_CATEGORIES, ALL_PICKER_ENTRIES, searchPickerEntries } from '../index';
|
|
41
4
|
|
|
42
5
|
describe('emojiData — unified picker entries', () => {
|
|
43
6
|
it('appends FA Brands, Solid, and Regular categories after the emoji buckets', () => {
|
|
@@ -78,12 +41,9 @@ describe('EmojiPicker', () => {
|
|
|
78
41
|
|
|
79
42
|
it('renders all emoji + FA tabs when open', () => {
|
|
80
43
|
render(<EmojiPicker open onSelect={() => {}} onClose={() => {}} />);
|
|
81
|
-
for (const cat of
|
|
44
|
+
for (const cat of PICKER_CATEGORIES) {
|
|
82
45
|
expect(screen.getByRole('tab', { name: cat.label })).toBeTruthy();
|
|
83
46
|
}
|
|
84
|
-
expect(screen.getByRole('tab', { name: 'Brands' })).toBeTruthy();
|
|
85
|
-
expect(screen.getByRole('tab', { name: 'Solid' })).toBeTruthy();
|
|
86
|
-
expect(screen.getByRole('tab', { name: 'Regular' })).toBeTruthy();
|
|
87
47
|
});
|
|
88
48
|
|
|
89
49
|
it('fires onSelect with an emoji PickerEntry when an emoji tile is clicked', () => {
|
|
@@ -4,7 +4,6 @@ import {
|
|
|
4
4
|
setHeadingLineTransition,
|
|
5
5
|
readBlockAttrsTransition,
|
|
6
6
|
setHeadingAttrsTransition,
|
|
7
|
-
setBlockAttrsTransition,
|
|
8
7
|
EMPTY_TRANSITION,
|
|
9
8
|
} from '../headingTransition';
|
|
10
9
|
|
|
@@ -174,15 +173,4 @@ describe('block-attrs (WYSIWYG) helpers', () => {
|
|
|
174
173
|
templateParams: null,
|
|
175
174
|
});
|
|
176
175
|
});
|
|
177
|
-
|
|
178
|
-
it('legacy single-channel writer still writes a fresh inner when none exists', () => {
|
|
179
|
-
expect(setBlockAttrsTransition(null, { type: 'fade', direction: '', duration: '' })).toBe(
|
|
180
|
-
'transition=fade',
|
|
181
|
-
);
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it('legacy single-channel writer preserves the id and clears to null when emptied', () => {
|
|
185
|
-
expect(setBlockAttrsTransition('transition=fade', EMPTY_TRANSITION)).toBeNull();
|
|
186
|
-
expect(setBlockAttrsTransition('#intro transition=fade', EMPTY_TRANSITION)).toBe('#intro');
|
|
187
|
-
});
|
|
188
176
|
});
|
|
@@ -182,8 +182,10 @@ describe('<EditorShell> image-edit modal wiring', () => {
|
|
|
182
182
|
|
|
183
183
|
expect(screen.queryByTestId('image-edit-modal')).toBeNull();
|
|
184
184
|
|
|
185
|
+
const trigger = screen.getByTestId('trigger');
|
|
186
|
+
trigger.focus();
|
|
185
187
|
await act(async () => {
|
|
186
|
-
fireEvent.click(
|
|
188
|
+
fireEvent.click(trigger);
|
|
187
189
|
});
|
|
188
190
|
|
|
189
191
|
await waitFor(() => {
|
|
@@ -191,6 +193,7 @@ describe('<EditorShell> image-edit modal wiring', () => {
|
|
|
191
193
|
});
|
|
192
194
|
// Shows the relative path in the header.
|
|
193
195
|
expect(screen.getByText('hero.png')).toBeTruthy();
|
|
196
|
+
expect(document.activeElement).toBe(screen.getByTestId('image-edit-modal-close'));
|
|
194
197
|
|
|
195
198
|
await act(async () => {
|
|
196
199
|
fireEvent.click(screen.getByTestId('image-edit-modal-close'));
|
|
@@ -198,6 +201,7 @@ describe('<EditorShell> image-edit modal wiring', () => {
|
|
|
198
201
|
await waitFor(() => {
|
|
199
202
|
expect(screen.queryByTestId('image-edit-modal')).toBeNull();
|
|
200
203
|
});
|
|
204
|
+
expect(document.activeElement).toBe(screen.getByTestId('trigger'));
|
|
201
205
|
});
|
|
202
206
|
|
|
203
207
|
it('opens the modal with a transient sidecar when only a mediaProvider is wired (no host container)', async () => {
|
|
@@ -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,7 +6,7 @@
|
|
|
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
12
|
import { DARK_SURFACE } from '@bendyline/squisq/schemas';
|
|
@@ -56,6 +56,20 @@ describe('<ImageEditor>', () => {
|
|
|
56
56
|
expect(screen.getByText(/loading image editor/i)).toBeTruthy();
|
|
57
57
|
});
|
|
58
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
|
+
|
|
59
73
|
it('applies an explicit dark surface to the editor chrome', async () => {
|
|
60
74
|
const parent = new MemoryContentContainer();
|
|
61
75
|
const sidecar = scopeContainer(parent, 'pic_files');
|
|
@@ -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
|
+
});
|
|
@@ -7,7 +7,7 @@ import { markdownToTiptap, tiptapToMarkdown } from '../tiptapBridge';
|
|
|
7
7
|
* uploaded files into the bin without inserting a markdown ref into
|
|
8
8
|
* the editor body. A user would upload an image, hit Send in the
|
|
9
9
|
* downstream chat composer, and the outgoing markdown would have no
|
|
10
|
-
* image reference — the
|
|
10
|
+
* image reference — the downstream consumer would reply "nothing came through."
|
|
11
11
|
*
|
|
12
12
|
* The fix: after `mediaProvider.addMedia(...)` succeeds, MediaBin
|
|
13
13
|
* fires `onMediaUploaded(relativePath, name, mimeType)`. The
|
|
@@ -18,7 +18,7 @@ import { markdownToTiptap, tiptapToMarkdown } from '../tiptapBridge';
|
|
|
18
18
|
* These tests exercise the contract directly: the markdown snippet
|
|
19
19
|
* produced by the upload callback, once round-tripped through the
|
|
20
20
|
* editor's markdown↔HTML bridge, must round-trip back to a form
|
|
21
|
-
* the
|
|
21
|
+
* the downstream service's image-extraction regex can see.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
function fakeMediaProvider(records: string[]): MediaProvider {
|