@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
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/** @vitest-environment jsdom */
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
4
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import type { Doc } from '@bendyline/squisq/schemas';
|
|
6
|
+
|
|
7
|
+
vi.mock('@bendyline/squisq-react', async (importOriginal) => {
|
|
8
|
+
const actual = await importOriginal<typeof import('@bendyline/squisq-react')>();
|
|
9
|
+
const blockSummary = (doc: Doc) =>
|
|
10
|
+
JSON.stringify(doc.blocks.map((block) => ({ id: block.id, template: block.template })));
|
|
11
|
+
return {
|
|
12
|
+
...actual,
|
|
13
|
+
useMediaProvider: () => null,
|
|
14
|
+
DocPlayer: ({ doc }: { doc: Doc }) => (
|
|
15
|
+
<div data-testid="mock-doc-player">{blockSummary(doc)}</div>
|
|
16
|
+
),
|
|
17
|
+
LinearDocView: ({ doc }: { doc: Doc }) => (
|
|
18
|
+
<div data-testid="mock-linear-doc">{blockSummary(doc)}</div>
|
|
19
|
+
),
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
vi.mock('../PlainHtmlPreview', () => ({
|
|
24
|
+
PlainHtmlPreview: ({ markdown }: { markdown: string }) => (
|
|
25
|
+
<div data-testid="mock-plain-document">{markdown}</div>
|
|
26
|
+
),
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
import { EditorProvider, useEditorContext } from '../EditorContext';
|
|
30
|
+
import { PreviewSettingsProvider, usePreviewSettings } from '../PreviewControls';
|
|
31
|
+
import { PreviewPanel } from '../PreviewPanel';
|
|
32
|
+
|
|
33
|
+
const SOURCE = `---
|
|
34
|
+
title: Metrics Brief
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
# Metrics Brief {[title]}
|
|
38
|
+
|
|
39
|
+
This report collects the strongest signals from a year of product work.
|
|
40
|
+
|
|
41
|
+
## Growth
|
|
42
|
+
|
|
43
|
+
Revenue increased 42% year over year while customer retention reached 91%.
|
|
44
|
+
|
|
45
|
+
## Milestone
|
|
46
|
+
|
|
47
|
+
On January 15, 2026 the team shipped the largest release in company history.
|
|
48
|
+
|
|
49
|
+
## Customer voice
|
|
50
|
+
|
|
51
|
+
> "The new workflow saves our team hours every week."
|
|
52
|
+
`;
|
|
53
|
+
|
|
54
|
+
function TransformButton() {
|
|
55
|
+
const { setSelectedTransformStyle } = usePreviewSettings();
|
|
56
|
+
return <button onClick={() => setSelectedTransformStyle('data-driven')}>Summarize now</button>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function Harness({ children }: { children?: ReactNode }) {
|
|
60
|
+
const { doc } = useEditorContext();
|
|
61
|
+
return (
|
|
62
|
+
<PreviewSettingsProvider doc={doc}>
|
|
63
|
+
<TransformButton />
|
|
64
|
+
{children}
|
|
65
|
+
<PreviewPanel />
|
|
66
|
+
</PreviewSettingsProvider>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function renderMode(displayMode?: string) {
|
|
71
|
+
const source = displayMode
|
|
72
|
+
? SOURCE.replace('title: Metrics Brief', `title: Metrics Brief\ndisplay-mode: ${displayMode}`)
|
|
73
|
+
: SOURCE;
|
|
74
|
+
return render(
|
|
75
|
+
<EditorProvider initialMarkdown={source} initialView="preview">
|
|
76
|
+
<Harness />
|
|
77
|
+
</EditorProvider>,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
describe('PreviewPanel transform projection', () => {
|
|
82
|
+
it('replaces the slideshow deck when a summarization style is selected', async () => {
|
|
83
|
+
renderMode('slideshow');
|
|
84
|
+
await waitFor(() => expect(screen.getByTestId('mock-doc-player')).toBeTruthy());
|
|
85
|
+
expect(screen.getByTestId('mock-doc-player').textContent).not.toContain('transform-');
|
|
86
|
+
|
|
87
|
+
fireEvent.click(screen.getByRole('button', { name: 'Summarize now' }));
|
|
88
|
+
|
|
89
|
+
await waitFor(() =>
|
|
90
|
+
expect(screen.getByTestId('mock-doc-player').textContent).toContain('transform-'),
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('feeds the transformed content model to Page mode', async () => {
|
|
95
|
+
renderMode('page');
|
|
96
|
+
await waitFor(() => expect(screen.getByTestId('mock-linear-doc')).toBeTruthy());
|
|
97
|
+
|
|
98
|
+
fireEvent.click(screen.getByRole('button', { name: 'Summarize now' }));
|
|
99
|
+
|
|
100
|
+
await waitFor(() =>
|
|
101
|
+
expect(screen.getByTestId('mock-linear-doc').textContent).toContain('transform-'),
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('feeds readable transformed Markdown to Document mode', async () => {
|
|
106
|
+
renderMode('document');
|
|
107
|
+
await waitFor(() => expect(screen.getByTestId('mock-plain-document')).toBeTruthy());
|
|
108
|
+
|
|
109
|
+
fireEvent.click(screen.getByRole('button', { name: 'Summarize now' }));
|
|
110
|
+
|
|
111
|
+
await waitFor(() => {
|
|
112
|
+
const markdown = screen.getByTestId('mock-plain-document').textContent ?? '';
|
|
113
|
+
expect(markdown).toContain('Metrics Brief');
|
|
114
|
+
expect(markdown).toContain('42%');
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { canHandleSquisqMediaDrop, ownsMonacoModel } from '../rawEditorIsolation';
|
|
3
|
+
import { SQUISQ_MEDIA_MIME } from '../mediaDragMime';
|
|
4
|
+
|
|
5
|
+
describe('RawEditor Monaco provider isolation', () => {
|
|
6
|
+
it('accepts only the model owned by the registering editor', () => {
|
|
7
|
+
const firstModel = {} as never;
|
|
8
|
+
const secondModel = {} as never;
|
|
9
|
+
const firstEditor = { getModel: () => firstModel } as never;
|
|
10
|
+
expect(ownsMonacoModel(firstEditor, firstModel)).toBe(true);
|
|
11
|
+
expect(ownsMonacoModel(firstEditor, secondModel)).toBe(false);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('rejects media-bin drops while read-only', () => {
|
|
15
|
+
const transfer = { types: [SQUISQ_MEDIA_MIME] } as unknown as DataTransfer;
|
|
16
|
+
expect(canHandleSquisqMediaDrop(true, transfer)).toBe(false);
|
|
17
|
+
expect(canHandleSquisqMediaDrop(false, transfer)).toBe(true);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
5
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
6
|
+
import type { MediaProvider } from '@bendyline/squisq/schemas';
|
|
7
|
+
import { RecorderModal } from '../recorder/RecorderModal';
|
|
8
|
+
import { RecorderPanel } from '../recorder/RecorderPanel';
|
|
9
|
+
|
|
10
|
+
const mediaProvider: MediaProvider = {
|
|
11
|
+
resolveUrl: vi.fn(async (path: string) => path),
|
|
12
|
+
listMedia: vi.fn(async () => []),
|
|
13
|
+
addMedia: vi.fn(async (name: string) => name),
|
|
14
|
+
removeMedia: vi.fn(async () => undefined),
|
|
15
|
+
dispose: vi.fn(),
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
describe('recorder theme propagation', () => {
|
|
19
|
+
it('creates a dark theme scope for the recorder dialog', () => {
|
|
20
|
+
render(<RecorderModal mediaProvider={mediaProvider} colorScheme="dark" onClose={vi.fn()} />);
|
|
21
|
+
|
|
22
|
+
const dialog = screen.getByRole('dialog', { name: 'Record media' });
|
|
23
|
+
expect(dialog.getAttribute('data-theme')).toBe('dark');
|
|
24
|
+
expect(dialog.classList.contains('squisq-editor-shell')).toBe(true);
|
|
25
|
+
expect(dialog.style.colorScheme).toBe('dark');
|
|
26
|
+
expect(dialog.style.getPropertyValue('--squisq-recorder-surface')).toBe(
|
|
27
|
+
'var(--squisq-bg, #1f2937)',
|
|
28
|
+
);
|
|
29
|
+
expect(dialog.style.getPropertyValue('--squisq-recorder-text')).toBe(
|
|
30
|
+
'var(--squisq-text, #e5e7eb)',
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('passes the requested scheme through the portaled panel wrapper', () => {
|
|
35
|
+
render(<RecorderPanel mediaProvider={mediaProvider} colorScheme="dark" />);
|
|
36
|
+
fireEvent.click(screen.getByRole('button', { name: 'Record media' }));
|
|
37
|
+
|
|
38
|
+
expect(screen.getByRole('dialog', { name: 'Record media' }).getAttribute('data-theme')).toBe(
|
|
39
|
+
'dark',
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { DEFAULT_THEME, VIEWPORT_PRESETS } from '@bendyline/squisq/schemas';
|
|
3
|
+
import type { Block, Doc, TextLayer } from '@bendyline/squisq/schemas';
|
|
4
|
+
import { resolveBlockVisual } from '../resolveBlockVisual';
|
|
5
|
+
|
|
6
|
+
describe('resolveBlockVisual', () => {
|
|
7
|
+
it('materializes document-scoped custom templates in editor previews', () => {
|
|
8
|
+
const block: Block = {
|
|
9
|
+
id: 'custom',
|
|
10
|
+
startTime: 0,
|
|
11
|
+
duration: 5,
|
|
12
|
+
audioSegment: 0,
|
|
13
|
+
template: 'hero',
|
|
14
|
+
title: 'Editor custom template',
|
|
15
|
+
};
|
|
16
|
+
const doc: Doc = {
|
|
17
|
+
articleId: 'custom-preview',
|
|
18
|
+
duration: 5,
|
|
19
|
+
blocks: [block],
|
|
20
|
+
audio: { segments: [] },
|
|
21
|
+
customTemplates: [
|
|
22
|
+
{
|
|
23
|
+
name: 'hero',
|
|
24
|
+
label: 'Hero',
|
|
25
|
+
viewport: { width: 1920, height: 1080 },
|
|
26
|
+
layers: [
|
|
27
|
+
{
|
|
28
|
+
id: 'hero-title',
|
|
29
|
+
type: 'text',
|
|
30
|
+
position: { x: '5%', y: '10%', width: '90%' },
|
|
31
|
+
content: { text: '{title}', style: { fontSize: 48, color: '#000000' } },
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const visual = resolveBlockVisual(doc, block, DEFAULT_THEME, VIEWPORT_PRESETS.landscape);
|
|
39
|
+
|
|
40
|
+
expect(visual).not.toBeNull();
|
|
41
|
+
expect((visual?.layers?.[0] as TextLayer).content.text).toBe('Editor custom template');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
selectionToTable,
|
|
4
|
+
selectionToTableMarkdown,
|
|
5
|
+
selectionToTaskItems,
|
|
6
|
+
selectionToTaskListMarkdown,
|
|
7
|
+
} from '../selectionConversions';
|
|
8
|
+
|
|
9
|
+
describe('selectionToTable', () => {
|
|
10
|
+
it('detects consistently pipe-delimited rows, including outer pipes', () => {
|
|
11
|
+
expect(selectionToTable('| Name | Role |\n| Ada | Engineer |')).toEqual({
|
|
12
|
+
delimiter: 'pipe',
|
|
13
|
+
rows: [
|
|
14
|
+
['Name', 'Role'],
|
|
15
|
+
['Ada', 'Engineer'],
|
|
16
|
+
],
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('detects CSV rows without splitting quoted commas', () => {
|
|
21
|
+
expect(selectionToTable('Name,Notes\nAda,"Math, logic"')).toEqual({
|
|
22
|
+
delimiter: 'comma',
|
|
23
|
+
rows: [
|
|
24
|
+
['Name', 'Notes'],
|
|
25
|
+
['Ada', 'Math, logic'],
|
|
26
|
+
],
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('retains literal quotes in comma-delimited cells', () => {
|
|
31
|
+
expect(selectionToTable('Item,Size\nDisplay,5" screen').rows[1]).toEqual([
|
|
32
|
+
'Display',
|
|
33
|
+
'5" screen',
|
|
34
|
+
]);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it.each([
|
|
38
|
+
['tab', 'Name\tRole\nAda\tEngineer'],
|
|
39
|
+
['multispace', 'Name Role\nAda Engineer'],
|
|
40
|
+
] as const)('detects %s-delimited rows', (delimiter, text) => {
|
|
41
|
+
expect(selectionToTable(text)).toEqual({
|
|
42
|
+
delimiter,
|
|
43
|
+
rows: [
|
|
44
|
+
['Name', 'Role'],
|
|
45
|
+
['Ada', 'Engineer'],
|
|
46
|
+
],
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('falls back to one column when delimiter counts are inconsistent', () => {
|
|
51
|
+
expect(selectionToTable('One,Two\nThree\nFour,Five,Six')).toEqual({
|
|
52
|
+
delimiter: null,
|
|
53
|
+
rows: [['One,Two'], ['Three'], ['Four,Five,Six']],
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('uses the first row as the Markdown header and escapes cell pipes', () => {
|
|
58
|
+
expect(selectionToTableMarkdown('Name,Notes\nAda,A | B')).toBe(
|
|
59
|
+
'| Name | Notes |\n| --- | --- |\n| Ada | A \\| B |',
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('selectionToTaskListMarkdown', () => {
|
|
65
|
+
it('turns non-empty selected lines into unchecked tasks', () => {
|
|
66
|
+
expect(selectionToTaskListMarkdown('Buy milk\n\nCall Sam')).toBe(
|
|
67
|
+
'- [ ] Buy milk\n- [ ] Call Sam',
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('normalizes list markers and retains existing checked state', () => {
|
|
72
|
+
const text = '- first\n2. second\n- [x] already done';
|
|
73
|
+
expect(selectionToTaskItems(text)).toEqual([
|
|
74
|
+
{ checked: false, text: 'first' },
|
|
75
|
+
{ checked: false, text: 'second' },
|
|
76
|
+
{ checked: true, text: 'already done' },
|
|
77
|
+
]);
|
|
78
|
+
expect(selectionToTaskListMarkdown(text)).toBe('- [ ] first\n- [ ] second\n- [x] already done');
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { EditorProvider } from '../EditorContext';
|
|
4
|
+
import { StatusBar } from '../StatusBar';
|
|
5
|
+
|
|
6
|
+
function renderStatusBar(markdown: string) {
|
|
7
|
+
return render(
|
|
8
|
+
<EditorProvider initialMarkdown={markdown} articleId="status-bar-test">
|
|
9
|
+
<StatusBar />
|
|
10
|
+
</EditorProvider>,
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('StatusBar', () => {
|
|
15
|
+
it('counts nested document blocks, not only top-level roots', async () => {
|
|
16
|
+
renderStatusBar('# Root\n\n## Child one\n\n### Grandchild\n\n## Child two\n');
|
|
17
|
+
|
|
18
|
+
expect(await screen.findByText('4 blocks')).toBeTruthy();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('uses the singular label for one block', async () => {
|
|
22
|
+
renderStatusBar('# Only block\n');
|
|
23
|
+
|
|
24
|
+
expect(await screen.findByText('1 block')).toBeTruthy();
|
|
25
|
+
expect(screen.queryByText('1 blocks')).toBeNull();
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
|
|
6
|
+
import { parseMarkdown } from '@bendyline/squisq/markdown';
|
|
7
|
+
import { markdownToDoc } from '@bendyline/squisq/doc';
|
|
8
|
+
import { buildNarrationScript } from '@bendyline/squisq/narration';
|
|
9
|
+
import { DEFAULT_THEME } from '@bendyline/squisq/schemas';
|
|
10
|
+
import { TeleprompterView } from '../teleprompter/TeleprompterView';
|
|
11
|
+
import { stepScroll, targetOffsetFor, type TokenLineMap } from '../teleprompter/scrollModel';
|
|
12
|
+
import { detectFloatTiers } from '../teleprompter/floatingWindow';
|
|
13
|
+
import { PCM_WORKLET_SOURCE, PCM_WORKLET_NAME } from '../teleprompter/pcmWorklet';
|
|
14
|
+
import { vadConfigForSensitivity } from '../teleprompter/useTeleprompter';
|
|
15
|
+
|
|
16
|
+
const MD = `# First Section
|
|
17
|
+
|
|
18
|
+
One two three four five six seven eight.
|
|
19
|
+
|
|
20
|
+
## Second Section
|
|
21
|
+
|
|
22
|
+
Nine ten eleven twelve.
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
cleanup();
|
|
27
|
+
// Prefs persist to localStorage; keep tests order-independent.
|
|
28
|
+
window.localStorage.removeItem('squisq:teleprompter-prefs');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('scrollModel', () => {
|
|
32
|
+
const lines: TokenLineMap = {
|
|
33
|
+
tokenTops: [0, 0, 0, 60, 60, 120, 120, 180],
|
|
34
|
+
tokenHeights: [60, 60, 60, 60, 60, 60, 60, 60],
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
it('places the active line at the eye-line', () => {
|
|
38
|
+
// Token 5 sits at top 120; eye-line = 35% of 600 = 210 → offset keeps it clamped ≥ 0.
|
|
39
|
+
expect(targetOffsetFor(5, lines, 600)).toBe(0);
|
|
40
|
+
// Smaller viewport pushes real scrolling: 120 + 30 − 0.35×200 = 80.
|
|
41
|
+
expect(targetOffsetFor(5, lines, 200)).toBe(80);
|
|
42
|
+
// Fractional positions interpolate between token tops.
|
|
43
|
+
expect(targetOffsetFor(4.5, lines, 200)).toBe(60 + 30 + 30 - 70);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('clamps to non-negative offsets and empty maps', () => {
|
|
47
|
+
expect(targetOffsetFor(0, lines, 600)).toBe(0);
|
|
48
|
+
expect(targetOffsetFor(3, { tokenTops: [], tokenHeights: [] }, 600)).toBe(0);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('stepScroll approaches the target and respects the speed clamp', () => {
|
|
52
|
+
// A huge jump advances by at most maxPxPerSec × dt.
|
|
53
|
+
expect(stepScroll(0, 100000, 16, 2600)).toBeCloseTo(2600 * 0.016, 5);
|
|
54
|
+
// Small distances converge and snap exactly.
|
|
55
|
+
let offset = 0;
|
|
56
|
+
for (let i = 0; i < 120; i++) offset = stepScroll(offset, 300, 16);
|
|
57
|
+
expect(offset).toBe(300);
|
|
58
|
+
// No time → no movement.
|
|
59
|
+
expect(stepScroll(50, 300, 0)).toBe(50);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('detectFloatTiers', () => {
|
|
64
|
+
it('reports popup + docked under jsdom (no PiP APIs)', () => {
|
|
65
|
+
const tiers = detectFloatTiers();
|
|
66
|
+
expect(tiers).toContain('popup');
|
|
67
|
+
expect(tiers[tiers.length - 1]).toBe('docked');
|
|
68
|
+
expect(tiers).not.toContain('document-pip');
|
|
69
|
+
expect(tiers).not.toContain('video-pip');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('reports document-pip first when the API exists', () => {
|
|
73
|
+
const win = window as Window & { documentPictureInPicture?: unknown };
|
|
74
|
+
win.documentPictureInPicture = { requestWindow: () => Promise.reject(new Error('stub')) };
|
|
75
|
+
try {
|
|
76
|
+
expect(detectFloatTiers()[0]).toBe('document-pip');
|
|
77
|
+
} finally {
|
|
78
|
+
delete win.documentPictureInPicture;
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('pcmWorklet source', () => {
|
|
84
|
+
it('compiles under stubbed worklet globals and registers the tap', () => {
|
|
85
|
+
const registered: string[] = [];
|
|
86
|
+
const factory = new Function(
|
|
87
|
+
'registerProcessor',
|
|
88
|
+
'AudioWorkletProcessor',
|
|
89
|
+
'currentTime',
|
|
90
|
+
PCM_WORKLET_SOURCE,
|
|
91
|
+
);
|
|
92
|
+
class ProcessorStub {
|
|
93
|
+
port = { postMessage: () => undefined };
|
|
94
|
+
}
|
|
95
|
+
factory((name: string) => registered.push(name), ProcessorStub, 0);
|
|
96
|
+
expect(registered).toEqual([PCM_WORKLET_NAME]);
|
|
97
|
+
expect(PCM_WORKLET_SOURCE).toContain('process(inputs)');
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('vadConfigForSensitivity', () => {
|
|
102
|
+
it('maps 0.5 to the engine defaults and keeps hysteresis ordering', () => {
|
|
103
|
+
const mid = vadConfigForSensitivity(0.5);
|
|
104
|
+
expect(mid.enterRatio).toBeCloseTo(3.0, 5);
|
|
105
|
+
for (const s of [0, 0.25, 0.5, 0.75, 1]) {
|
|
106
|
+
const cfg = vadConfigForSensitivity(s);
|
|
107
|
+
expect(cfg.enterRatio!).toBeGreaterThan(cfg.exitRatio! - 1e-9);
|
|
108
|
+
}
|
|
109
|
+
// Higher sensitivity → lower (easier) enter threshold.
|
|
110
|
+
expect(vadConfigForSensitivity(0.9).enterRatio!).toBeLessThan(
|
|
111
|
+
vadConfigForSensitivity(0.1).enterRatio!,
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('TeleprompterView', () => {
|
|
117
|
+
function renderView(markdown: string | null) {
|
|
118
|
+
const doc = markdown ? markdownToDoc(parseMarkdown(markdown)) : null;
|
|
119
|
+
render(<TeleprompterView doc={doc} theme={DEFAULT_THEME} />);
|
|
120
|
+
return doc;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
it('renders one span per script token plus block markers and controls', () => {
|
|
124
|
+
const doc = renderView(MD);
|
|
125
|
+
const script = buildNarrationScript(doc!);
|
|
126
|
+
const spans = document.querySelectorAll('[data-token-idx]');
|
|
127
|
+
expect(spans.length).toBe(script.tokens.length);
|
|
128
|
+
expect(
|
|
129
|
+
screen.getByText('First Section', { selector: '.squisq-teleprompter-block-marker' }),
|
|
130
|
+
).toBeTruthy();
|
|
131
|
+
expect(screen.getByTestId('teleprompter-controls')).toBeTruthy();
|
|
132
|
+
expect(screen.getByRole('button', { name: 'Start prompter' })).toBeTruthy();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('mirrors the live surface into a presentation target without duplicating controls', () => {
|
|
136
|
+
const doc = markdownToDoc(parseMarkdown(MD));
|
|
137
|
+
const audienceDocument = document.implementation.createHTMLDocument('Audience');
|
|
138
|
+
const audienceTarget = audienceDocument.createElement('div');
|
|
139
|
+
audienceDocument.body.appendChild(audienceTarget);
|
|
140
|
+
|
|
141
|
+
render(
|
|
142
|
+
<TeleprompterView doc={doc} theme={DEFAULT_THEME} presentationTarget={audienceTarget} />,
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
expect(audienceTarget.querySelector('[aria-label="Audience presentation"]')).toBeTruthy();
|
|
146
|
+
expect(audienceTarget.querySelectorAll('[data-token-idx]').length).toBe(
|
|
147
|
+
buildNarrationScript(doc).tokens.length,
|
|
148
|
+
);
|
|
149
|
+
expect(audienceTarget.querySelector('[data-testid="teleprompter-controls"]')).toBeNull();
|
|
150
|
+
expect(screen.getAllByTestId('teleprompter-controls')).toHaveLength(1);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('shows an empty state for an empty doc', () => {
|
|
154
|
+
renderView(null);
|
|
155
|
+
expect(screen.getByText(/Nothing to narrate yet/)).toBeTruthy();
|
|
156
|
+
expect(screen.queryByTestId('teleprompter-controls')).toBeNull();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('mirrors the empty state into a presentation target', () => {
|
|
160
|
+
const audienceDocument = document.implementation.createHTMLDocument('Audience');
|
|
161
|
+
const audienceTarget = audienceDocument.createElement('div');
|
|
162
|
+
audienceDocument.body.appendChild(audienceTarget);
|
|
163
|
+
|
|
164
|
+
render(
|
|
165
|
+
<TeleprompterView doc={null} theme={DEFAULT_THEME} presentationTarget={audienceTarget} />,
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
expect(audienceTarget.querySelector('[aria-label="Audience presentation"]')).toBeTruthy();
|
|
169
|
+
expect(audienceTarget.textContent).toContain('Nothing to narrate yet');
|
|
170
|
+
expect(audienceTarget.querySelector('[data-testid="teleprompter-controls"]')).toBeNull();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('start button enters countdown and Escape cancels back to stopped', () => {
|
|
174
|
+
renderView(MD);
|
|
175
|
+
fireEvent.click(screen.getByRole('button', { name: 'Start prompter' }));
|
|
176
|
+
// Default prefs use a 3 s countdown → the overlay digit appears.
|
|
177
|
+
expect(document.querySelector('.squisq-teleprompter-countdown-digit')?.textContent).toBe('3');
|
|
178
|
+
fireEvent.keyDown(screen.getByTestId('teleprompter-view'), { key: 'Escape' });
|
|
179
|
+
expect(document.querySelector('.squisq-teleprompter-countdown-digit')).toBeNull();
|
|
180
|
+
expect(screen.getByRole('button', { name: 'Start prompter' })).toBeTruthy();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('mirror toggle flips the surface class', () => {
|
|
184
|
+
renderView(MD);
|
|
185
|
+
const surface = screen.getByTestId('teleprompter-surface');
|
|
186
|
+
expect(surface.className).not.toContain('--mirrored');
|
|
187
|
+
fireEvent.click(screen.getByRole('button', { name: '⇋ Mirror' }));
|
|
188
|
+
expect(screen.getByTestId('teleprompter-surface').className).toContain('--mirrored');
|
|
189
|
+
});
|
|
190
|
+
});
|
|
@@ -2,6 +2,15 @@ import { describe, it, expect } from 'vitest';
|
|
|
2
2
|
import { markdownToTiptap, tiptapToMarkdown } from '../tiptapBridge';
|
|
3
3
|
|
|
4
4
|
describe('Template annotation round-trip', () => {
|
|
5
|
+
it('hides an empty annotation in Write view while preserving it on round-trip', () => {
|
|
6
|
+
const original = '## Getting Started {[]}';
|
|
7
|
+
const html = markdownToTiptap(original);
|
|
8
|
+
|
|
9
|
+
expect(html).toContain('data-template-empty="true"');
|
|
10
|
+
expect(html).not.toContain('{[]}');
|
|
11
|
+
expect(tiptapToMarkdown(html).trim()).toBe(original);
|
|
12
|
+
});
|
|
13
|
+
|
|
5
14
|
it('extracts {[template]} into data-template on markdownToTiptap', () => {
|
|
6
15
|
const html = markdownToTiptap('## Getting Started {[comparisonBar]}');
|
|
7
16
|
expect(html).toContain('data-template="comparisonBar"');
|
|
@@ -16,6 +25,23 @@ describe('Template annotation round-trip', () => {
|
|
|
16
25
|
expect(back.trim()).toBe(original);
|
|
17
26
|
});
|
|
18
27
|
|
|
28
|
+
it('preserves a heading whose last word is also the template label', () => {
|
|
29
|
+
const original = '### A Famous Quote {[quote]}';
|
|
30
|
+
const html = markdownToTiptap(original);
|
|
31
|
+
|
|
32
|
+
expect(tiptapToMarkdown(html).trim()).toBe(original);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('ignores legacy text rendered inside a template badge', () => {
|
|
36
|
+
const tiptapRendered =
|
|
37
|
+
'<h3 data-template="quote">' +
|
|
38
|
+
'<span class="squisq-heading-content">A Famous Quote</span>' +
|
|
39
|
+
'<span class="squisq-template-badge" contenteditable="false">Quote</span>' +
|
|
40
|
+
'</h3>';
|
|
41
|
+
|
|
42
|
+
expect(tiptapToMarkdown(tiptapRendered).trim()).toBe('### A Famous Quote {[quote]}');
|
|
43
|
+
});
|
|
44
|
+
|
|
19
45
|
it('round-trips param-only squiggly annotations', () => {
|
|
20
46
|
const original = '## Intro {[transition=fade]}';
|
|
21
47
|
const html = markdownToTiptap(original);
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { cleanup, render } from '@testing-library/react';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { TEMPLATE_GALLERY_PORTAL_SELECTOR, TemplateBadgePopover } from '../TemplatePicker';
|
|
4
|
+
|
|
5
|
+
const anchorRect = new DOMRect(20, 20, 80, 24);
|
|
6
|
+
const originalElementsFromPoint = document.elementsFromPoint;
|
|
7
|
+
|
|
8
|
+
function badge(key: string) {
|
|
9
|
+
return (
|
|
10
|
+
<TemplateBadgePopover
|
|
11
|
+
key={key}
|
|
12
|
+
anchorRect={anchorRect}
|
|
13
|
+
value=""
|
|
14
|
+
onChange={vi.fn()}
|
|
15
|
+
onClose={vi.fn()}
|
|
16
|
+
/>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
Object.defineProperty(document, 'elementsFromPoint', {
|
|
22
|
+
configurable: true,
|
|
23
|
+
value: () => [],
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
cleanup();
|
|
29
|
+
Object.defineProperty(document, 'elementsFromPoint', {
|
|
30
|
+
configurable: true,
|
|
31
|
+
value: originalElementsFromPoint,
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('template gallery portal ids', () => {
|
|
36
|
+
it('uses an instance-scoped id for a single open heading-badge gallery', () => {
|
|
37
|
+
render(badge('only'));
|
|
38
|
+
|
|
39
|
+
const gallery = document.querySelector(TEMPLATE_GALLERY_PORTAL_SELECTOR);
|
|
40
|
+
expect(gallery).not.toBeNull();
|
|
41
|
+
expect(gallery?.id).toMatch(/^squisq-template-gallery-portal-\S+$/);
|
|
42
|
+
expect(gallery?.id).not.toBe('squisq-template-gallery-portal');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('keeps concurrent editor galleries unique and preserves the survivor id', () => {
|
|
46
|
+
const view = render(
|
|
47
|
+
<>
|
|
48
|
+
{badge('first')}
|
|
49
|
+
{badge('second')}
|
|
50
|
+
</>,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const galleries = [...document.querySelectorAll(TEMPLATE_GALLERY_PORTAL_SELECTOR)];
|
|
54
|
+
expect(galleries).toHaveLength(2);
|
|
55
|
+
expect(new Set(galleries.map((gallery) => gallery.id)).size).toBe(2);
|
|
56
|
+
const survivorId = galleries[1].id;
|
|
57
|
+
|
|
58
|
+
view.rerender(<>{badge('second')}</>);
|
|
59
|
+
const survivor = document.querySelector(TEMPLATE_GALLERY_PORTAL_SELECTOR);
|
|
60
|
+
expect(survivor?.id).toBe(survivorId);
|
|
61
|
+
});
|
|
62
|
+
});
|