@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,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useRef } from 'react';
|
|
6
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
7
|
+
import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
|
|
8
|
+
import { EditorProvider } from '../EditorContext';
|
|
9
|
+
import {
|
|
10
|
+
PresentationModeControl,
|
|
11
|
+
PresentationModeProvider,
|
|
12
|
+
} from '../presentation/PresentationMode';
|
|
13
|
+
|
|
14
|
+
function Harness() {
|
|
15
|
+
const rootRef = useRef<HTMLDivElement>(null);
|
|
16
|
+
return (
|
|
17
|
+
<div ref={rootRef} data-testid="shell">
|
|
18
|
+
<PresentationModeProvider rootRef={rootRef}>
|
|
19
|
+
<PresentationModeControl />
|
|
20
|
+
<div data-testid="content">Preview</div>
|
|
21
|
+
</PresentationModeProvider>
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function renderHarness() {
|
|
27
|
+
return render(
|
|
28
|
+
<EditorProvider initialMarkdown="# Presentation" initialView="preview">
|
|
29
|
+
<Harness />
|
|
30
|
+
</EditorProvider>,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
cleanup();
|
|
36
|
+
vi.restoreAllMocks();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('PresentationModeControl', () => {
|
|
40
|
+
it('chooses the launch behavior from an accessible radio menu', () => {
|
|
41
|
+
renderHarness();
|
|
42
|
+
|
|
43
|
+
expect(screen.getByRole('button', { name: 'Present: Fill Squisq' })).toBeTruthy();
|
|
44
|
+
fireEvent.click(screen.getByRole('button', { name: 'Presentation options' }));
|
|
45
|
+
|
|
46
|
+
const menu = screen.getByRole('menu', { name: 'Presentation options' });
|
|
47
|
+
const items = within(menu).getAllByRole('menuitemradio');
|
|
48
|
+
expect(items.map((item) => item.textContent)).toEqual([
|
|
49
|
+
'Fill SquisqUse the entire Squisq control.',
|
|
50
|
+
'New windowOpen an audience view synced to this one.',
|
|
51
|
+
'Browser full screenBrowser full screen is unavailable.',
|
|
52
|
+
]);
|
|
53
|
+
expect(items[0].getAttribute('aria-checked')).toBe('true');
|
|
54
|
+
expect(items.map((item) => item.tabIndex)).toEqual([0, -1, -1]);
|
|
55
|
+
|
|
56
|
+
fireEvent.keyDown(items[0], { key: 'ArrowDown' });
|
|
57
|
+
expect(document.activeElement).toBe(items[1]);
|
|
58
|
+
const tabEvent = new KeyboardEvent('keydown', {
|
|
59
|
+
key: 'Tab',
|
|
60
|
+
bubbles: true,
|
|
61
|
+
cancelable: true,
|
|
62
|
+
});
|
|
63
|
+
fireEvent(items[1], tabEvent);
|
|
64
|
+
expect(tabEvent.defaultPrevented).toBe(true);
|
|
65
|
+
expect(screen.queryByRole('menu', { name: 'Presentation options' })).toBeNull();
|
|
66
|
+
expect(document.activeElement).toBe(
|
|
67
|
+
screen.getByRole('button', { name: 'Presentation options' }),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
fireEvent.click(screen.getByRole('button', { name: 'Presentation options' }));
|
|
71
|
+
const reopenedMenu = screen.getByRole('menu', { name: 'Presentation options' });
|
|
72
|
+
|
|
73
|
+
fireEvent.click(within(reopenedMenu).getByRole('menuitemradio', { name: /New window/ }));
|
|
74
|
+
expect(screen.getByRole('button', { name: 'Present: New window' })).toBeTruthy();
|
|
75
|
+
expect(screen.queryByRole('menu', { name: 'Presentation options' })).toBeNull();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('fills only the Squisq shell and exits on Escape', async () => {
|
|
79
|
+
renderHarness();
|
|
80
|
+
const shell = screen.getByTestId('shell');
|
|
81
|
+
const launchButton = screen.getByRole('button', { name: 'Present: Fill Squisq' });
|
|
82
|
+
|
|
83
|
+
launchButton.focus();
|
|
84
|
+
fireEvent.click(launchButton);
|
|
85
|
+
expect(shell.getAttribute('data-presentation-mode')).toBe('control');
|
|
86
|
+
const exitButton = screen.getByRole('button', { name: 'Exit presentation mode' });
|
|
87
|
+
expect(document.activeElement).toBe(exitButton);
|
|
88
|
+
|
|
89
|
+
fireEvent.keyDown(document, { key: 'Escape' });
|
|
90
|
+
await waitFor(() => expect(shell.hasAttribute('data-presentation-mode')).toBe(false));
|
|
91
|
+
expect(screen.getByRole('button', { name: 'Present: Fill Squisq' })).toBeTruthy();
|
|
92
|
+
expect(document.activeElement).toBe(launchButton);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('tracks native fullscreen exit from the browser', async () => {
|
|
96
|
+
const originalRequest = Object.getOwnPropertyDescriptor(
|
|
97
|
+
HTMLElement.prototype,
|
|
98
|
+
'requestFullscreen',
|
|
99
|
+
);
|
|
100
|
+
const originalExit = Object.getOwnPropertyDescriptor(document, 'exitFullscreen');
|
|
101
|
+
const originalElement = Object.getOwnPropertyDescriptor(document, 'fullscreenElement');
|
|
102
|
+
let fullscreenElement: Element | null = null;
|
|
103
|
+
const requestFullscreen = vi.fn(() => Promise.resolve());
|
|
104
|
+
Object.defineProperty(HTMLElement.prototype, 'requestFullscreen', {
|
|
105
|
+
configurable: true,
|
|
106
|
+
value: requestFullscreen,
|
|
107
|
+
});
|
|
108
|
+
Object.defineProperty(document, 'exitFullscreen', {
|
|
109
|
+
configurable: true,
|
|
110
|
+
value: vi.fn(async () => {
|
|
111
|
+
fullscreenElement = null;
|
|
112
|
+
}),
|
|
113
|
+
});
|
|
114
|
+
Object.defineProperty(document, 'fullscreenElement', {
|
|
115
|
+
configurable: true,
|
|
116
|
+
get: () => fullscreenElement,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
renderHarness();
|
|
121
|
+
fireEvent.click(screen.getByRole('button', { name: 'Presentation options' }));
|
|
122
|
+
fireEvent.click(
|
|
123
|
+
screen.getByRole('menuitemradio', { name: /Browser full screen.*Use the browser/ }),
|
|
124
|
+
);
|
|
125
|
+
fireEvent.click(screen.getByRole('button', { name: 'Present: Browser full screen' }));
|
|
126
|
+
|
|
127
|
+
const shell = screen.getByTestId('shell');
|
|
128
|
+
await waitFor(() => expect(requestFullscreen).toHaveBeenCalledWith());
|
|
129
|
+
await waitFor(() => expect(shell.getAttribute('data-presentation-mode')).toBe('fullscreen'));
|
|
130
|
+
|
|
131
|
+
fullscreenElement = null;
|
|
132
|
+
fireEvent(document, new Event('fullscreenchange'));
|
|
133
|
+
await waitFor(() => expect(shell.hasAttribute('data-presentation-mode')).toBe(false));
|
|
134
|
+
} finally {
|
|
135
|
+
if (originalRequest) {
|
|
136
|
+
Object.defineProperty(HTMLElement.prototype, 'requestFullscreen', originalRequest);
|
|
137
|
+
} else {
|
|
138
|
+
Reflect.deleteProperty(HTMLElement.prototype, 'requestFullscreen');
|
|
139
|
+
}
|
|
140
|
+
if (originalExit) Object.defineProperty(document, 'exitFullscreen', originalExit);
|
|
141
|
+
else Reflect.deleteProperty(document, 'exitFullscreen');
|
|
142
|
+
if (originalElement) Object.defineProperty(document, 'fullscreenElement', originalElement);
|
|
143
|
+
else Reflect.deleteProperty(document, 'fullscreenElement');
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('keeps fullscreen presentation active when browser exit is rejected', async () => {
|
|
148
|
+
const originalRequest = Object.getOwnPropertyDescriptor(
|
|
149
|
+
HTMLElement.prototype,
|
|
150
|
+
'requestFullscreen',
|
|
151
|
+
);
|
|
152
|
+
const originalExit = Object.getOwnPropertyDescriptor(document, 'exitFullscreen');
|
|
153
|
+
const originalElement = Object.getOwnPropertyDescriptor(document, 'fullscreenElement');
|
|
154
|
+
let fullscreenElement: Element | null = null;
|
|
155
|
+
let requestedRoot: Element | null = null;
|
|
156
|
+
Object.defineProperty(HTMLElement.prototype, 'requestFullscreen', {
|
|
157
|
+
configurable: true,
|
|
158
|
+
value: vi.fn(async () => {
|
|
159
|
+
fullscreenElement = requestedRoot;
|
|
160
|
+
}),
|
|
161
|
+
});
|
|
162
|
+
Object.defineProperty(document, 'exitFullscreen', {
|
|
163
|
+
configurable: true,
|
|
164
|
+
value: vi.fn(async () => {
|
|
165
|
+
throw new Error('exit denied');
|
|
166
|
+
}),
|
|
167
|
+
});
|
|
168
|
+
Object.defineProperty(document, 'fullscreenElement', {
|
|
169
|
+
configurable: true,
|
|
170
|
+
get: () => fullscreenElement,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
renderHarness();
|
|
175
|
+
requestedRoot = screen.getByTestId('shell');
|
|
176
|
+
fireEvent.click(screen.getByRole('button', { name: 'Presentation options' }));
|
|
177
|
+
fireEvent.click(screen.getByRole('menuitemradio', { name: /Browser full screen/ }));
|
|
178
|
+
fireEvent.click(screen.getByRole('button', { name: 'Present: Browser full screen' }));
|
|
179
|
+
await waitFor(() =>
|
|
180
|
+
expect(screen.getByTestId('shell').getAttribute('data-presentation-mode')).toBe(
|
|
181
|
+
'fullscreen',
|
|
182
|
+
),
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
fireEvent.click(screen.getByRole('button', { name: 'Stop presentation' }));
|
|
186
|
+
await waitFor(() =>
|
|
187
|
+
expect(screen.getByRole('alert').textContent).toContain('could not exit full screen'),
|
|
188
|
+
);
|
|
189
|
+
expect(screen.getByTestId('shell').getAttribute('data-presentation-mode')).toBe('fullscreen');
|
|
190
|
+
|
|
191
|
+
fullscreenElement = null;
|
|
192
|
+
fireEvent(document, new Event('fullscreenchange'));
|
|
193
|
+
} finally {
|
|
194
|
+
if (originalRequest) {
|
|
195
|
+
Object.defineProperty(HTMLElement.prototype, 'requestFullscreen', originalRequest);
|
|
196
|
+
} else {
|
|
197
|
+
Reflect.deleteProperty(HTMLElement.prototype, 'requestFullscreen');
|
|
198
|
+
}
|
|
199
|
+
if (originalExit) Object.defineProperty(document, 'exitFullscreen', originalExit);
|
|
200
|
+
else Reflect.deleteProperty(document, 'exitFullscreen');
|
|
201
|
+
if (originalElement) Object.defineProperty(document, 'fullscreenElement', originalElement);
|
|
202
|
+
else Reflect.deleteProperty(document, 'fullscreenElement');
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('opens a same-origin audience window and cleans up when it closes', async () => {
|
|
207
|
+
const popupDocument = document.implementation.createHTMLDocument('');
|
|
208
|
+
const popupEvents = new EventTarget();
|
|
209
|
+
let closed = false;
|
|
210
|
+
const popup = {
|
|
211
|
+
document: popupDocument,
|
|
212
|
+
get closed() {
|
|
213
|
+
return closed;
|
|
214
|
+
},
|
|
215
|
+
close: vi.fn(() => {
|
|
216
|
+
closed = true;
|
|
217
|
+
}),
|
|
218
|
+
focus: vi.fn(),
|
|
219
|
+
addEventListener: popupEvents.addEventListener.bind(popupEvents),
|
|
220
|
+
removeEventListener: popupEvents.removeEventListener.bind(popupEvents),
|
|
221
|
+
} as unknown as Window;
|
|
222
|
+
vi.spyOn(window, 'open').mockReturnValue(popup);
|
|
223
|
+
|
|
224
|
+
renderHarness();
|
|
225
|
+
fireEvent.click(screen.getByRole('button', { name: 'Presentation options' }));
|
|
226
|
+
fireEvent.click(screen.getByRole('menuitemradio', { name: /New window/ }));
|
|
227
|
+
fireEvent.click(screen.getByRole('button', { name: 'Present: New window' }));
|
|
228
|
+
|
|
229
|
+
const shell = screen.getByTestId('shell');
|
|
230
|
+
await waitFor(() => expect(shell.getAttribute('data-presentation-mode')).toBe('window'));
|
|
231
|
+
expect(popupDocument.querySelector('#squisq-presentation-root')).toBeTruthy();
|
|
232
|
+
expect(popupDocument.querySelector('[aria-label="Exit presentation mode"]')).toBeTruthy();
|
|
233
|
+
|
|
234
|
+
// Re-selecting the checked destination only closes the menu; it must not
|
|
235
|
+
// tear down the active audience window.
|
|
236
|
+
fireEvent.click(screen.getByRole('button', { name: 'Presentation options' }));
|
|
237
|
+
fireEvent.click(screen.getByRole('menuitemradio', { name: /New window/ }));
|
|
238
|
+
expect(shell.getAttribute('data-presentation-mode')).toBe('window');
|
|
239
|
+
expect(popup.close).not.toHaveBeenCalled();
|
|
240
|
+
|
|
241
|
+
popupEvents.dispatchEvent(new Event('pagehide'));
|
|
242
|
+
await waitFor(() => expect(shell.hasAttribute('data-presentation-mode')).toBe(false));
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('reports a blocked audience window without entering presentation', async () => {
|
|
246
|
+
vi.spyOn(window, 'open').mockReturnValue(null);
|
|
247
|
+
renderHarness();
|
|
248
|
+
fireEvent.click(screen.getByRole('button', { name: 'Presentation options' }));
|
|
249
|
+
fireEvent.click(screen.getByRole('menuitemradio', { name: /New window/ }));
|
|
250
|
+
fireEvent.click(screen.getByRole('button', { name: 'Present: New window' }));
|
|
251
|
+
|
|
252
|
+
await waitFor(() =>
|
|
253
|
+
expect(screen.getByRole('alert').textContent).toContain('presentation window was blocked'),
|
|
254
|
+
);
|
|
255
|
+
expect(screen.getByTestId('shell').hasAttribute('data-presentation-mode')).toBe(false);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('cleans up when popup initialization fails after the portal is prepared', async () => {
|
|
259
|
+
const popupDocument = document.implementation.createHTMLDocument('');
|
|
260
|
+
const popupEvents = new EventTarget();
|
|
261
|
+
let closed = false;
|
|
262
|
+
const popup = {
|
|
263
|
+
document: popupDocument,
|
|
264
|
+
get closed() {
|
|
265
|
+
return closed;
|
|
266
|
+
},
|
|
267
|
+
close: vi.fn(() => {
|
|
268
|
+
closed = true;
|
|
269
|
+
}),
|
|
270
|
+
focus: vi.fn(() => {
|
|
271
|
+
throw new Error('focus failed');
|
|
272
|
+
}),
|
|
273
|
+
addEventListener: popupEvents.addEventListener.bind(popupEvents),
|
|
274
|
+
removeEventListener: popupEvents.removeEventListener.bind(popupEvents),
|
|
275
|
+
} as unknown as Window;
|
|
276
|
+
vi.spyOn(window, 'open').mockReturnValue(popup);
|
|
277
|
+
|
|
278
|
+
renderHarness();
|
|
279
|
+
fireEvent.click(screen.getByRole('button', { name: 'Presentation options' }));
|
|
280
|
+
fireEvent.click(screen.getByRole('menuitemradio', { name: /New window/ }));
|
|
281
|
+
fireEvent.click(screen.getByRole('button', { name: 'Present: New window' }));
|
|
282
|
+
|
|
283
|
+
await waitFor(() =>
|
|
284
|
+
expect(screen.getByRole('alert').textContent).toContain('presentation window was blocked'),
|
|
285
|
+
);
|
|
286
|
+
expect(screen.getByTestId('shell').hasAttribute('data-presentation-mode')).toBe(false);
|
|
287
|
+
expect(popup.close).toHaveBeenCalledOnce();
|
|
288
|
+
expect(popupDocument.querySelector('[aria-label="Exit presentation mode"]')).toBeNull();
|
|
289
|
+
});
|
|
290
|
+
});
|
|
@@ -2,13 +2,34 @@
|
|
|
2
2
|
* @vitest-environment jsdom
|
|
3
3
|
*/
|
|
4
4
|
import { afterEach, describe, expect, it } from 'vitest';
|
|
5
|
-
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
5
|
+
import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
|
|
6
|
+
import { compileTheme } from '@bendyline/squisq/schemas';
|
|
6
7
|
import { EditorProvider, useEditorContext } from '../EditorContext';
|
|
7
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
PreviewModeSwitch,
|
|
10
|
+
PreviewSettingsProvider,
|
|
11
|
+
PreviewToolbarControls,
|
|
12
|
+
usePreviewSettings,
|
|
13
|
+
} from '../PreviewControls';
|
|
14
|
+
import { Toolbar } from '../Toolbar';
|
|
15
|
+
import {
|
|
16
|
+
clearThemeLibrary,
|
|
17
|
+
CustomThemeProvider,
|
|
18
|
+
saveLibraryTheme,
|
|
19
|
+
useDocCustomThemes,
|
|
20
|
+
} from '../customThemes';
|
|
8
21
|
|
|
9
22
|
function ModeProbe() {
|
|
10
|
-
const { activeDisplayMode } = usePreviewSettings();
|
|
11
|
-
return
|
|
23
|
+
const { activeDisplayMode, activeThemeId, activeTransformStyle } = usePreviewSettings();
|
|
24
|
+
return (
|
|
25
|
+
<div
|
|
26
|
+
data-testid="active-mode"
|
|
27
|
+
data-theme-id={activeThemeId}
|
|
28
|
+
data-transform-style={activeTransformStyle}
|
|
29
|
+
>
|
|
30
|
+
{activeDisplayMode}
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
12
33
|
}
|
|
13
34
|
|
|
14
35
|
function PreviewHarness() {
|
|
@@ -21,6 +42,52 @@ function PreviewHarness() {
|
|
|
21
42
|
);
|
|
22
43
|
}
|
|
23
44
|
|
|
45
|
+
function PreviewToolbarHarness() {
|
|
46
|
+
const { doc } = useEditorContext();
|
|
47
|
+
return (
|
|
48
|
+
<PreviewSettingsProvider doc={doc}>
|
|
49
|
+
<PreviewToolbarControls />
|
|
50
|
+
</PreviewSettingsProvider>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function UseTabHarness() {
|
|
55
|
+
const { activeView, doc } = useEditorContext();
|
|
56
|
+
return (
|
|
57
|
+
<PreviewSettingsProvider doc={doc}>
|
|
58
|
+
<Toolbar />
|
|
59
|
+
<ModeProbe />
|
|
60
|
+
<div data-testid="active-view">{activeView}</div>
|
|
61
|
+
</PreviewSettingsProvider>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function LibraryThemeProbe() {
|
|
66
|
+
const { activeTheme, setSelectedThemeId } = usePreviewSettings();
|
|
67
|
+
const { markdownSource } = useEditorContext();
|
|
68
|
+
return (
|
|
69
|
+
<>
|
|
70
|
+
<button type="button" onClick={() => setSelectedThemeId('library-theme')}>
|
|
71
|
+
Select library theme
|
|
72
|
+
</button>
|
|
73
|
+
<div data-testid="active-theme-name">{activeTheme.name}</div>
|
|
74
|
+
<pre data-testid="markdown-source">{markdownSource}</pre>
|
|
75
|
+
</>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function LibraryThemeHarness() {
|
|
80
|
+
const { doc } = useEditorContext();
|
|
81
|
+
const { docThemes, onDocThemesChange } = useDocCustomThemes();
|
|
82
|
+
return (
|
|
83
|
+
<CustomThemeProvider docThemes={docThemes} onDocThemesChange={onDocThemesChange}>
|
|
84
|
+
<PreviewSettingsProvider doc={doc}>
|
|
85
|
+
<LibraryThemeProbe />
|
|
86
|
+
</PreviewSettingsProvider>
|
|
87
|
+
</CustomThemeProvider>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
24
91
|
function renderPreviewControls(markdown: string) {
|
|
25
92
|
render(
|
|
26
93
|
<EditorProvider initialMarkdown={markdown}>
|
|
@@ -29,7 +96,18 @@ function renderPreviewControls(markdown: string) {
|
|
|
29
96
|
);
|
|
30
97
|
}
|
|
31
98
|
|
|
32
|
-
|
|
99
|
+
function renderPreviewToolbar(markdown: string) {
|
|
100
|
+
render(
|
|
101
|
+
<EditorProvider initialMarkdown={markdown}>
|
|
102
|
+
<PreviewToolbarHarness />
|
|
103
|
+
</EditorProvider>,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
afterEach(() => {
|
|
108
|
+
cleanup();
|
|
109
|
+
clearThemeLibrary();
|
|
110
|
+
});
|
|
33
111
|
|
|
34
112
|
describe('PreviewModeSwitch', () => {
|
|
35
113
|
it('labels the plain document preview as Document and the styled view as Page', () => {
|
|
@@ -40,7 +118,8 @@ describe('PreviewModeSwitch', () => {
|
|
|
40
118
|
.map((button) => button.textContent)
|
|
41
119
|
.filter(Boolean);
|
|
42
120
|
|
|
43
|
-
expect(labels).toEqual(['
|
|
121
|
+
expect(labels).toEqual(['Slideshow', 'Video', 'Page', 'Document', 'Narrate']);
|
|
122
|
+
expect(screen.getByTestId('active-mode').textContent).toBe('slideshow');
|
|
44
123
|
|
|
45
124
|
fireEvent.click(screen.getByRole('button', { name: 'Document' }));
|
|
46
125
|
expect(screen.getByTestId('active-mode').textContent).toBe('page');
|
|
@@ -67,4 +146,231 @@ describe('PreviewModeSwitch', () => {
|
|
|
67
146
|
});
|
|
68
147
|
expect(screen.getByRole('button', { name: 'Page' }).getAttribute('aria-pressed')).toBe('true');
|
|
69
148
|
});
|
|
149
|
+
|
|
150
|
+
it('uses the shared squisq-theme, themeId, theme fallback order', async () => {
|
|
151
|
+
renderPreviewControls('---\nthemeId: bold\ntheme: cinematic\n---\n\n# Hello');
|
|
152
|
+
|
|
153
|
+
await waitFor(() => {
|
|
154
|
+
expect(screen.getByTestId('active-mode').getAttribute('data-theme-id')).toBe('bold');
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('canonicalizes the stored dataDriven transform id', async () => {
|
|
159
|
+
renderPreviewControls('---\nsquisq-transform: dataDriven\n---\n\n# Hello');
|
|
160
|
+
|
|
161
|
+
await waitFor(() => {
|
|
162
|
+
expect(screen.getByTestId('active-mode').getAttribute('data-transform-style')).toBe(
|
|
163
|
+
'data-driven',
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('copies a selected browser-library theme into the document atomically', async () => {
|
|
169
|
+
saveLibraryTheme(
|
|
170
|
+
compileTheme({
|
|
171
|
+
id: 'library-theme',
|
|
172
|
+
name: 'Library Theme',
|
|
173
|
+
seedColors: { primary: '#456789' },
|
|
174
|
+
}),
|
|
175
|
+
);
|
|
176
|
+
render(
|
|
177
|
+
<EditorProvider initialMarkdown="# Hello">
|
|
178
|
+
<LibraryThemeHarness />
|
|
179
|
+
</EditorProvider>,
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
fireEvent.click(screen.getByRole('button', { name: 'Select library theme' }));
|
|
183
|
+
|
|
184
|
+
await waitFor(() => {
|
|
185
|
+
expect(screen.getByTestId('active-theme-name').textContent).toBe('Library Theme');
|
|
186
|
+
const source = screen.getByTestId('markdown-source').textContent ?? '';
|
|
187
|
+
expect(source).toContain('squisq-theme: library-theme');
|
|
188
|
+
expect(source).toContain('squisq-custom-themes:');
|
|
189
|
+
expect(source).toContain('library-theme');
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe('PreviewToolbarControls', () => {
|
|
195
|
+
it('presents transforms as summarization without implying the source is changed', () => {
|
|
196
|
+
const originalResizeObserver = globalThis.ResizeObserver;
|
|
197
|
+
class ResizeObserverStub implements ResizeObserver {
|
|
198
|
+
observe() {}
|
|
199
|
+
unobserve() {}
|
|
200
|
+
disconnect() {}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
globalThis.ResizeObserver = ResizeObserverStub;
|
|
204
|
+
try {
|
|
205
|
+
renderPreviewToolbar('# Hello');
|
|
206
|
+
|
|
207
|
+
const summarizeLabels = screen.getAllByText('Summarize:');
|
|
208
|
+
expect(summarizeLabels.length).toBeGreaterThan(0);
|
|
209
|
+
for (const label of summarizeLabels) {
|
|
210
|
+
expect(label.getAttribute('title')).toBe(
|
|
211
|
+
'Extract and summarize content for presentation with these Use modes. Your underlying content is not changed.',
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
expect(screen.queryByText('Transform:')).toBeNull();
|
|
215
|
+
expect(document.querySelector('[role="group"][aria-label="Display mode"]')).toBeNull();
|
|
216
|
+
expect(document.querySelector('[role="group"][aria-label="Aspect ratio"]')).not.toBeNull();
|
|
217
|
+
} finally {
|
|
218
|
+
if (originalResizeObserver) {
|
|
219
|
+
globalThis.ResizeObserver = originalResizeObserver;
|
|
220
|
+
} else {
|
|
221
|
+
Reflect.deleteProperty(globalThis, 'ResizeObserver');
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('keeps the overflow popover inside the left viewport edge', async () => {
|
|
227
|
+
const originalResizeObserver = globalThis.ResizeObserver;
|
|
228
|
+
const originalGetBoundingClientRect = HTMLElement.prototype.getBoundingClientRect;
|
|
229
|
+
const originalClientWidth = Object.getOwnPropertyDescriptor(
|
|
230
|
+
HTMLElement.prototype,
|
|
231
|
+
'clientWidth',
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
class ResizeObserverStub implements ResizeObserver {
|
|
235
|
+
readonly callback: ResizeObserverCallback;
|
|
236
|
+
|
|
237
|
+
constructor(callback: ResizeObserverCallback) {
|
|
238
|
+
this.callback = callback;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
observe() {
|
|
242
|
+
this.callback([], this);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
unobserve() {}
|
|
246
|
+
|
|
247
|
+
disconnect() {}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
globalThis.ResizeObserver = ResizeObserverStub;
|
|
251
|
+
HTMLElement.prototype.getBoundingClientRect = function getBoundingClientRect() {
|
|
252
|
+
if (this.classList.contains('squisq-preview-controls-popover')) {
|
|
253
|
+
return new DOMRect(0, 0, 220, 240);
|
|
254
|
+
}
|
|
255
|
+
if (this.getAttribute('aria-label') === 'More preview settings') {
|
|
256
|
+
return new DOMRect(6, 20, 28, 28);
|
|
257
|
+
}
|
|
258
|
+
if (this.classList.contains('squisq-preview-control')) {
|
|
259
|
+
return new DOMRect(0, 0, 100, 24);
|
|
260
|
+
}
|
|
261
|
+
return new DOMRect(0, 0, 0, 0);
|
|
262
|
+
};
|
|
263
|
+
Object.defineProperty(HTMLElement.prototype, 'clientWidth', {
|
|
264
|
+
configurable: true,
|
|
265
|
+
get() {
|
|
266
|
+
return this.classList.contains('squisq-preview-controls') ? 40 : 0;
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
renderPreviewToolbar('# Hello');
|
|
272
|
+
|
|
273
|
+
fireEvent.click(await screen.findByRole('button', { name: 'More preview settings' }));
|
|
274
|
+
|
|
275
|
+
await waitFor(() => {
|
|
276
|
+
const popover = document.querySelector<HTMLElement>('.squisq-preview-controls-popover');
|
|
277
|
+
expect(popover).not.toBeNull();
|
|
278
|
+
expect(popover?.style.left).toBe('8px');
|
|
279
|
+
});
|
|
280
|
+
} finally {
|
|
281
|
+
if (originalResizeObserver) {
|
|
282
|
+
globalThis.ResizeObserver = originalResizeObserver;
|
|
283
|
+
} else {
|
|
284
|
+
Reflect.deleteProperty(globalThis, 'ResizeObserver');
|
|
285
|
+
}
|
|
286
|
+
HTMLElement.prototype.getBoundingClientRect = originalGetBoundingClientRect;
|
|
287
|
+
if (originalClientWidth) {
|
|
288
|
+
Object.defineProperty(HTMLElement.prototype, 'clientWidth', originalClientWidth);
|
|
289
|
+
} else {
|
|
290
|
+
Reflect.deleteProperty(HTMLElement.prototype, 'clientWidth');
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
describe('Use tab mode menu', () => {
|
|
297
|
+
it('lists the Use modes beside the tab and selects a mode without a toolbar radio group', () => {
|
|
298
|
+
render(
|
|
299
|
+
<EditorProvider initialMarkdown="# Hello" initialView="wysiwyg" allowRecording={false}>
|
|
300
|
+
<UseTabHarness />
|
|
301
|
+
</EditorProvider>,
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
const useTab = screen.getByRole('tab', { name: 'Slideshow' });
|
|
305
|
+
expect(screen.queryByRole('tab', { name: 'Use' })).toBeNull();
|
|
306
|
+
const tabGroup = useTab.parentElement;
|
|
307
|
+
expect(tabGroup).not.toBeNull();
|
|
308
|
+
expect(within(tabGroup!).getByRole('button', { name: 'Choose Use mode' })).toBeTruthy();
|
|
309
|
+
|
|
310
|
+
// The first click enters Use; once active, clicking the tab again opens
|
|
311
|
+
// the mode menu.
|
|
312
|
+
fireEvent.click(useTab);
|
|
313
|
+
expect(screen.getByTestId('active-view').textContent).toBe('preview');
|
|
314
|
+
expect(screen.queryByRole('menu', { name: 'Use mode' })).toBeNull();
|
|
315
|
+
|
|
316
|
+
fireEvent.click(screen.getByRole('tab', { name: 'Slideshow' }));
|
|
317
|
+
|
|
318
|
+
const menu = screen.getByRole('menu', { name: 'Use mode' });
|
|
319
|
+
expect(
|
|
320
|
+
within(menu)
|
|
321
|
+
.getAllByRole('menuitemradio')
|
|
322
|
+
.map((item) => item.querySelector('.squisq-use-mode-menu-label')?.textContent),
|
|
323
|
+
).toEqual(['Slideshow', 'Video', 'Page', 'Document', 'Narrate']);
|
|
324
|
+
const slideshowItem = within(menu).getByRole('menuitemradio', { name: 'Slideshow' });
|
|
325
|
+
expect(slideshowItem.getAttribute('aria-checked')).toBe('true');
|
|
326
|
+
expect(within(slideshowItem).getByText('Present designed slides one at a time.')).toBeTruthy();
|
|
327
|
+
|
|
328
|
+
fireEvent.click(within(menu).getByRole('menuitemradio', { name: 'Document' }));
|
|
329
|
+
expect(screen.getByTestId('active-mode').textContent).toBe('page');
|
|
330
|
+
expect(screen.getByTestId('active-view').textContent).toBe('preview');
|
|
331
|
+
expect(screen.getByRole('tab', { name: 'Document' })).toBeTruthy();
|
|
332
|
+
expect(screen.queryByRole('menu', { name: 'Use mode' })).toBeNull();
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
describe('Narrate mode gating', () => {
|
|
337
|
+
function renderWithNarrateGate(markdown: string, allowNarrate: boolean) {
|
|
338
|
+
render(
|
|
339
|
+
<EditorProvider initialMarkdown={markdown} allowNarrate={allowNarrate}>
|
|
340
|
+
<PreviewHarness />
|
|
341
|
+
</EditorProvider>,
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
it('selects narrate from the switch and resolves teleprompter frontmatter aliases', async () => {
|
|
346
|
+
renderPreviewControls('# Hello');
|
|
347
|
+
fireEvent.click(screen.getByRole('button', { name: 'Narrate' }));
|
|
348
|
+
expect(screen.getByTestId('active-mode').textContent).toBe('narrate');
|
|
349
|
+
|
|
350
|
+
cleanup();
|
|
351
|
+
renderPreviewControls('---\ndisplay-mode: teleprompter\n---\n\n# Hello');
|
|
352
|
+
await waitFor(() => {
|
|
353
|
+
expect(screen.getByTestId('active-mode').textContent).toBe('narrate');
|
|
354
|
+
});
|
|
355
|
+
expect(screen.getByRole('button', { name: 'Narrate' }).getAttribute('aria-pressed')).toBe(
|
|
356
|
+
'true',
|
|
357
|
+
);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('allowNarrate=false hides the button and clamps frontmatter-forced narrate to video', async () => {
|
|
361
|
+
renderWithNarrateGate('---\ndisplay-mode: narrate\n---\n\n# Hello', false);
|
|
362
|
+
|
|
363
|
+
expect(screen.queryByRole('button', { name: 'Narrate' })).toBeNull();
|
|
364
|
+
// Give the frontmatter parse a tick, then confirm the clamp held.
|
|
365
|
+
await waitFor(() => {
|
|
366
|
+
expect(screen.getByTestId('active-mode').textContent).toBe('video');
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('allowNarrate=true (default) honors frontmatter narrate', async () => {
|
|
371
|
+
renderWithNarrateGate('---\ndisplay-mode: narrate\n---\n\n# Hello', true);
|
|
372
|
+
await waitFor(() => {
|
|
373
|
+
expect(screen.getByTestId('active-mode').textContent).toBe('narrate');
|
|
374
|
+
});
|
|
375
|
+
});
|
|
70
376
|
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** @vitest-environment jsdom */
|
|
2
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { MemoryContentContainer } from '@bendyline/squisq/storage';
|
|
5
|
+
|
|
6
|
+
const resolveAudioMapping = vi.hoisted(() =>
|
|
7
|
+
vi.fn(async () => {
|
|
8
|
+
throw new Error('timing.json is corrupt');
|
|
9
|
+
}),
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
vi.mock('@bendyline/squisq/doc', async (importOriginal) => ({
|
|
13
|
+
...(await importOriginal<typeof import('@bendyline/squisq/doc')>()),
|
|
14
|
+
resolveAudioMapping,
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
import { EditorProvider, useEditorContext } from '../EditorContext';
|
|
18
|
+
import { PreviewSettingsProvider } from '../PreviewControls';
|
|
19
|
+
import { PreviewPanel } from '../PreviewPanel';
|
|
20
|
+
|
|
21
|
+
function Harness({ container }: { container: MemoryContentContainer }) {
|
|
22
|
+
const { doc } = useEditorContext();
|
|
23
|
+
return (
|
|
24
|
+
<PreviewSettingsProvider doc={doc}>
|
|
25
|
+
<PreviewPanel workspaceContainer={container} />
|
|
26
|
+
</PreviewSettingsProvider>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('PreviewPanel optional audio mapping', () => {
|
|
31
|
+
it('keeps the synchronous preview when audio discovery rejects', async () => {
|
|
32
|
+
const load = vi.spyOn(HTMLMediaElement.prototype, 'load').mockImplementation(() => undefined);
|
|
33
|
+
const container = new MemoryContentContainer();
|
|
34
|
+
const view = render(
|
|
35
|
+
<EditorProvider initialMarkdown="# Preview\n\nBody" initialView="preview">
|
|
36
|
+
<Harness container={container} />
|
|
37
|
+
</EditorProvider>,
|
|
38
|
+
);
|
|
39
|
+
await waitFor(() => expect(resolveAudioMapping).toHaveBeenCalled());
|
|
40
|
+
await waitFor(() => {
|
|
41
|
+
expect(screen.getByTestId('preview-panel').textContent).not.toContain('No content');
|
|
42
|
+
});
|
|
43
|
+
view.unmount();
|
|
44
|
+
load.mockRestore();
|
|
45
|
+
});
|
|
46
|
+
});
|