@dfosco/storyboard-react 4.0.0-beta.8 → 4.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/package.json +6 -3
- package/src/AuthModal/AuthModal.jsx +134 -0
- package/src/AuthModal/AuthModal.module.css +221 -0
- package/src/BranchBar/BranchBar.jsx +56 -0
- package/src/BranchBar/BranchBar.module.css +230 -0
- package/src/BranchBar/useBranches.js +79 -0
- package/src/CommandPalette/CommandPalette.jsx +936 -0
- package/src/CommandPalette/CreateDialog.jsx +219 -0
- package/src/CommandPalette/command-palette.css +111 -0
- package/src/Icon.jsx +180 -0
- package/src/Viewfinder.jsx +1104 -57
- package/src/Viewfinder.module.css +1107 -149
- package/src/canvas/CanvasControls.jsx +51 -2
- package/src/canvas/CanvasControls.module.css +31 -0
- package/src/canvas/CanvasPage.bridge.test.jsx +142 -19
- package/src/canvas/CanvasPage.dragdrop.test.jsx +346 -0
- package/src/canvas/CanvasPage.jsx +807 -251
- package/src/canvas/CanvasPage.module.css +98 -50
- package/src/canvas/CanvasPage.multiselect.test.jsx +13 -11
- package/src/canvas/CanvasToolbar.jsx +2 -2
- package/src/canvas/MarqueeOverlay.jsx +20 -0
- package/src/canvas/PageSelector.jsx +239 -0
- package/src/canvas/PageSelector.module.css +165 -0
- package/src/canvas/PageSelector.test.jsx +104 -0
- package/src/canvas/canvasApi.js +22 -8
- package/src/canvas/canvasTheme.js +96 -52
- package/src/canvas/componentIsolate.jsx +33 -7
- package/src/canvas/useCanvas.js +9 -8
- package/src/canvas/useCanvas.test.js +4 -4
- package/src/canvas/useMarqueeSelect.js +187 -0
- package/src/canvas/useMarqueeSelect.test.js +78 -0
- package/src/canvas/widgets/CodePenEmbed.jsx +292 -0
- package/src/canvas/widgets/CodePenEmbed.module.css +161 -0
- package/src/canvas/widgets/ComponentWidget.jsx +42 -10
- package/src/canvas/widgets/ComponentWidget.module.css +6 -5
- package/src/canvas/widgets/FigmaEmbed.jsx +110 -24
- package/src/canvas/widgets/FigmaEmbed.module.css +21 -7
- package/src/canvas/widgets/LinkPreview.jsx +297 -11
- package/src/canvas/widgets/LinkPreview.module.css +386 -18
- package/src/canvas/widgets/LinkPreview.test.jsx +193 -0
- package/src/canvas/widgets/MarkdownBlock.jsx +86 -5
- package/src/canvas/widgets/MarkdownBlock.module.css +64 -15
- package/src/canvas/widgets/PrototypeEmbed.jsx +96 -145
- package/src/canvas/widgets/PrototypeEmbed.module.css +74 -4
- package/src/canvas/widgets/StickyNote.module.css +5 -0
- package/src/canvas/widgets/StickyNote.test.jsx +9 -9
- package/src/canvas/widgets/StoryWidget.jsx +277 -0
- package/src/canvas/widgets/StoryWidget.module.css +211 -0
- package/src/canvas/widgets/WidgetChrome.jsx +76 -20
- package/src/canvas/widgets/WidgetChrome.module.css +2 -6
- package/src/canvas/widgets/WidgetWrapper.module.css +2 -0
- package/src/canvas/widgets/codepenUrl.js +75 -0
- package/src/canvas/widgets/codepenUrl.test.js +76 -0
- package/src/canvas/widgets/embedInteraction.test.jsx +235 -0
- package/src/canvas/widgets/embedOverlay.module.css +35 -0
- package/src/canvas/widgets/embedTheme.js +138 -39
- package/src/canvas/widgets/githubUrl.js +82 -0
- package/src/canvas/widgets/githubUrl.test.js +74 -0
- package/src/canvas/widgets/iframeDevLogs.js +49 -0
- package/src/canvas/widgets/iframeDevLogs.test.jsx +81 -0
- package/src/canvas/widgets/index.js +4 -0
- package/src/canvas/widgets/pasteRules.js +295 -0
- package/src/canvas/widgets/pasteRules.test.js +474 -0
- package/src/canvas/widgets/snapshotDisplay.test.jsx +259 -0
- package/src/canvas/widgets/widgetConfig.js +16 -5
- package/src/canvas/widgets/widgetConfig.test.js +34 -12
- package/src/context.jsx +145 -16
- package/src/hooks/useSceneData.js +4 -2
- package/src/hooks/useThemeState.js +61 -0
- package/src/hooks/useThemeState.test.js +66 -0
- package/src/index.js +10 -0
- package/src/story/StoryPage.jsx +117 -0
- package/src/story/StoryPage.module.css +18 -0
- package/src/vite/data-plugin.js +348 -66
- package/src/vite/data-plugin.test.js +405 -5
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for canvas image drag-and-drop functionality.
|
|
3
|
+
*
|
|
4
|
+
* Tests the event handling for dropping images from Finder/file manager
|
|
5
|
+
* onto the canvas, including coordinate conversion and file filtering.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
8
|
+
import { fireEvent, render, act, waitFor } from '@testing-library/react'
|
|
9
|
+
import CanvasPage from './CanvasPage.jsx'
|
|
10
|
+
import { addWidget, uploadImage } from './canvasApi.js'
|
|
11
|
+
|
|
12
|
+
// Mock dependencies
|
|
13
|
+
vi.mock('@dfosco/tiny-canvas', () => ({
|
|
14
|
+
Canvas: ({ children }) => <div data-testid="tiny-canvas">{children}</div>,
|
|
15
|
+
}))
|
|
16
|
+
|
|
17
|
+
const mockCanvas = {
|
|
18
|
+
title: 'Drag Drop Test Canvas',
|
|
19
|
+
widgets: [],
|
|
20
|
+
sources: [],
|
|
21
|
+
centered: false,
|
|
22
|
+
dotted: false,
|
|
23
|
+
grid: true,
|
|
24
|
+
gridSize: 24,
|
|
25
|
+
colorMode: 'auto',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
vi.mock('./useCanvas.js', () => ({
|
|
29
|
+
useCanvas: () => ({
|
|
30
|
+
canvas: mockCanvas,
|
|
31
|
+
jsxExports: {},
|
|
32
|
+
loading: false,
|
|
33
|
+
}),
|
|
34
|
+
}))
|
|
35
|
+
|
|
36
|
+
vi.mock('./widgets/index.js', () => ({
|
|
37
|
+
getWidgetComponent: () => function MockWidget() {
|
|
38
|
+
return <div>mock widget</div>
|
|
39
|
+
},
|
|
40
|
+
}))
|
|
41
|
+
|
|
42
|
+
vi.mock('./widgets/WidgetChrome.jsx', () => ({
|
|
43
|
+
default: ({ children }) => <div data-testid="widget-chrome">{children}</div>,
|
|
44
|
+
}))
|
|
45
|
+
|
|
46
|
+
vi.mock('./widgets/widgetProps.js', () => ({
|
|
47
|
+
schemas: {},
|
|
48
|
+
getDefaults: () => ({}),
|
|
49
|
+
}))
|
|
50
|
+
|
|
51
|
+
vi.mock('./widgets/widgetConfig.js', () => ({
|
|
52
|
+
getFeatures: () => [],
|
|
53
|
+
isResizable: () => false,
|
|
54
|
+
schemas: {},
|
|
55
|
+
getMenuWidgetTypes: () => [],
|
|
56
|
+
}))
|
|
57
|
+
|
|
58
|
+
vi.mock('./widgets/figmaUrl.js', () => ({
|
|
59
|
+
isFigmaUrl: () => false,
|
|
60
|
+
sanitizeFigmaUrl: (url) => url,
|
|
61
|
+
}))
|
|
62
|
+
|
|
63
|
+
vi.mock('./canvasApi.js', () => ({
|
|
64
|
+
addWidget: vi.fn(() => Promise.resolve({ success: true, widget: { id: 'image-abc', type: 'image' } })),
|
|
65
|
+
updateCanvas: vi.fn(() => Promise.resolve({ success: true })),
|
|
66
|
+
removeWidget: vi.fn(),
|
|
67
|
+
uploadImage: vi.fn(() => Promise.resolve({ success: true, filename: 'test-image.png' })),
|
|
68
|
+
}))
|
|
69
|
+
|
|
70
|
+
vi.mock('./useUndoRedo.js', () => ({
|
|
71
|
+
default: () => ({
|
|
72
|
+
snapshot: vi.fn(),
|
|
73
|
+
undo: vi.fn(),
|
|
74
|
+
redo: vi.fn(),
|
|
75
|
+
reset: vi.fn(),
|
|
76
|
+
canUndo: false,
|
|
77
|
+
canRedo: false,
|
|
78
|
+
}),
|
|
79
|
+
}))
|
|
80
|
+
|
|
81
|
+
// Helper to create a mock File
|
|
82
|
+
function createMockImageFile(name = 'test.png', type = 'image/png') {
|
|
83
|
+
const blob = new Blob(['fake image data'], { type })
|
|
84
|
+
return new File([blob], name, { type })
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Helper to create a DataTransfer-like object
|
|
88
|
+
function createDataTransfer(files, types = ['Files']) {
|
|
89
|
+
return {
|
|
90
|
+
files,
|
|
91
|
+
types,
|
|
92
|
+
dropEffect: 'none',
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
describe('CanvasPage image drag-and-drop', () => {
|
|
97
|
+
beforeEach(() => {
|
|
98
|
+
vi.clearAllMocks()
|
|
99
|
+
// Mock FileReader for blobToDataUrl
|
|
100
|
+
global.FileReader = class {
|
|
101
|
+
readAsDataURL() {
|
|
102
|
+
setTimeout(() => {
|
|
103
|
+
this.result = 'data:image/png;base64,fakedata'
|
|
104
|
+
this.onload?.()
|
|
105
|
+
}, 0)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Mock Image for getImageDimensions
|
|
109
|
+
global.Image = class {
|
|
110
|
+
set src(val) {
|
|
111
|
+
setTimeout(() => {
|
|
112
|
+
this.naturalWidth = 800
|
|
113
|
+
this.naturalHeight = 600
|
|
114
|
+
this.onload?.()
|
|
115
|
+
}, 0)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
afterEach(() => {
|
|
121
|
+
delete global.FileReader
|
|
122
|
+
delete global.Image
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('allows drop by preventing default on dragover with Files', () => {
|
|
126
|
+
render(<CanvasPage canvasId="test-canvas" />)
|
|
127
|
+
const scrollContainer = document.querySelector('[data-storyboard-canvas-scroll]')
|
|
128
|
+
|
|
129
|
+
const dragOverEvent = new Event('dragover', { bubbles: true, cancelable: true })
|
|
130
|
+
dragOverEvent.dataTransfer = createDataTransfer([], ['Files'])
|
|
131
|
+
dragOverEvent.preventDefault = vi.fn()
|
|
132
|
+
|
|
133
|
+
scrollContainer.dispatchEvent(dragOverEvent)
|
|
134
|
+
|
|
135
|
+
expect(dragOverEvent.preventDefault).toHaveBeenCalled()
|
|
136
|
+
expect(dragOverEvent.dataTransfer.dropEffect).toBe('copy')
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('ignores dragover without Files type (internal widget drag)', () => {
|
|
140
|
+
render(<CanvasPage canvasId="test-canvas" />)
|
|
141
|
+
const scrollContainer = document.querySelector('[data-storyboard-canvas-scroll]')
|
|
142
|
+
|
|
143
|
+
const dragOverEvent = new Event('dragover', { bubbles: true, cancelable: true })
|
|
144
|
+
dragOverEvent.dataTransfer = createDataTransfer([], ['text/plain'])
|
|
145
|
+
dragOverEvent.preventDefault = vi.fn()
|
|
146
|
+
|
|
147
|
+
scrollContainer.dispatchEvent(dragOverEvent)
|
|
148
|
+
|
|
149
|
+
expect(dragOverEvent.preventDefault).not.toHaveBeenCalled()
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('uploads image and creates widget on drop', async () => {
|
|
153
|
+
render(<CanvasPage canvasId="test-canvas" />)
|
|
154
|
+
const scrollContainer = document.querySelector('[data-storyboard-canvas-scroll]')
|
|
155
|
+
|
|
156
|
+
const imageFile = createMockImageFile('photo.png', 'image/png')
|
|
157
|
+
const dropEvent = new Event('drop', { bubbles: true, cancelable: true })
|
|
158
|
+
dropEvent.dataTransfer = createDataTransfer([imageFile], ['Files'])
|
|
159
|
+
dropEvent.clientX = 200
|
|
160
|
+
dropEvent.clientY = 150
|
|
161
|
+
dropEvent.preventDefault = vi.fn()
|
|
162
|
+
dropEvent.stopPropagation = vi.fn()
|
|
163
|
+
|
|
164
|
+
// Mock getBoundingClientRect for coordinate calculation
|
|
165
|
+
scrollContainer.getBoundingClientRect = () => ({
|
|
166
|
+
left: 0,
|
|
167
|
+
top: 0,
|
|
168
|
+
width: 1000,
|
|
169
|
+
height: 800,
|
|
170
|
+
})
|
|
171
|
+
scrollContainer.scrollLeft = 0
|
|
172
|
+
scrollContainer.scrollTop = 0
|
|
173
|
+
|
|
174
|
+
await act(async () => {
|
|
175
|
+
scrollContainer.dispatchEvent(dropEvent)
|
|
176
|
+
// Wait for async processing
|
|
177
|
+
await new Promise((r) => setTimeout(r, 50))
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
expect(dropEvent.preventDefault).toHaveBeenCalled()
|
|
181
|
+
expect(uploadImage).toHaveBeenCalledWith(
|
|
182
|
+
expect.stringContaining('data:image/png'),
|
|
183
|
+
'test-canvas'
|
|
184
|
+
)
|
|
185
|
+
expect(addWidget).toHaveBeenCalledWith(
|
|
186
|
+
'test-canvas',
|
|
187
|
+
expect.objectContaining({
|
|
188
|
+
type: 'image',
|
|
189
|
+
props: expect.objectContaining({
|
|
190
|
+
src: 'test-image.png',
|
|
191
|
+
private: false,
|
|
192
|
+
}),
|
|
193
|
+
position: expect.objectContaining({
|
|
194
|
+
x: expect.any(Number),
|
|
195
|
+
y: expect.any(Number),
|
|
196
|
+
}),
|
|
197
|
+
})
|
|
198
|
+
)
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it('ignores non-image files but prevents browser default', async () => {
|
|
202
|
+
render(<CanvasPage canvasId="test-canvas" />)
|
|
203
|
+
const scrollContainer = document.querySelector('[data-storyboard-canvas-scroll]')
|
|
204
|
+
|
|
205
|
+
const textFile = new File(['text content'], 'readme.txt', { type: 'text/plain' })
|
|
206
|
+
const dropEvent = new Event('drop', { bubbles: true, cancelable: true })
|
|
207
|
+
dropEvent.dataTransfer = createDataTransfer([textFile], ['Files'])
|
|
208
|
+
dropEvent.clientX = 200
|
|
209
|
+
dropEvent.clientY = 150
|
|
210
|
+
dropEvent.preventDefault = vi.fn()
|
|
211
|
+
dropEvent.stopPropagation = vi.fn()
|
|
212
|
+
|
|
213
|
+
scrollContainer.getBoundingClientRect = () => ({ left: 0, top: 0 })
|
|
214
|
+
|
|
215
|
+
await act(async () => {
|
|
216
|
+
scrollContainer.dispatchEvent(dropEvent)
|
|
217
|
+
await new Promise((r) => setTimeout(r, 50))
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
// Should prevent default (stops browser from opening the file)
|
|
221
|
+
expect(dropEvent.preventDefault).toHaveBeenCalled()
|
|
222
|
+
// But should not call upload or add widget for non-image files
|
|
223
|
+
expect(uploadImage).not.toHaveBeenCalled()
|
|
224
|
+
expect(addWidget).not.toHaveBeenCalled()
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it('processes multiple image files on drop', async () => {
|
|
228
|
+
render(<CanvasPage canvasId="test-canvas" />)
|
|
229
|
+
const scrollContainer = document.querySelector('[data-storyboard-canvas-scroll]')
|
|
230
|
+
|
|
231
|
+
const image1 = createMockImageFile('photo1.png', 'image/png')
|
|
232
|
+
const image2 = createMockImageFile('photo2.jpg', 'image/jpeg')
|
|
233
|
+
const dropEvent = new Event('drop', { bubbles: true, cancelable: true })
|
|
234
|
+
dropEvent.dataTransfer = createDataTransfer([image1, image2], ['Files'])
|
|
235
|
+
dropEvent.clientX = 100
|
|
236
|
+
dropEvent.clientY = 100
|
|
237
|
+
dropEvent.preventDefault = vi.fn()
|
|
238
|
+
dropEvent.stopPropagation = vi.fn()
|
|
239
|
+
|
|
240
|
+
scrollContainer.getBoundingClientRect = () => ({ left: 0, top: 0, width: 1000, height: 800 })
|
|
241
|
+
scrollContainer.scrollLeft = 0
|
|
242
|
+
scrollContainer.scrollTop = 0
|
|
243
|
+
|
|
244
|
+
await act(async () => {
|
|
245
|
+
scrollContainer.dispatchEvent(dropEvent)
|
|
246
|
+
// Wait longer for multiple async operations
|
|
247
|
+
await new Promise((r) => setTimeout(r, 150))
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
// Should call upload for each image
|
|
251
|
+
expect(uploadImage).toHaveBeenCalledTimes(2)
|
|
252
|
+
expect(addWidget).toHaveBeenCalledTimes(2)
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
it('ignores drop without Files type', async () => {
|
|
256
|
+
render(<CanvasPage canvasId="test-canvas" />)
|
|
257
|
+
const scrollContainer = document.querySelector('[data-storyboard-canvas-scroll]')
|
|
258
|
+
|
|
259
|
+
const dropEvent = new Event('drop', { bubbles: true, cancelable: true })
|
|
260
|
+
dropEvent.dataTransfer = createDataTransfer([], ['text/html'])
|
|
261
|
+
dropEvent.preventDefault = vi.fn()
|
|
262
|
+
|
|
263
|
+
await act(async () => {
|
|
264
|
+
scrollContainer.dispatchEvent(dropEvent)
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
expect(dropEvent.preventDefault).not.toHaveBeenCalled()
|
|
268
|
+
expect(uploadImage).not.toHaveBeenCalled()
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
it('snaps drop position to grid when snap is enabled', async () => {
|
|
272
|
+
// Enable snap in mock data
|
|
273
|
+
const originalSnapToGrid = mockCanvas.snapToGrid
|
|
274
|
+
mockCanvas.snapToGrid = true
|
|
275
|
+
|
|
276
|
+
render(<CanvasPage canvasId="test-canvas" />)
|
|
277
|
+
const scrollContainer = document.querySelector('[data-storyboard-canvas-scroll]')
|
|
278
|
+
|
|
279
|
+
const imageFile = createMockImageFile()
|
|
280
|
+
const dropEvent = new Event('drop', { bubbles: true, cancelable: true })
|
|
281
|
+
dropEvent.dataTransfer = createDataTransfer([imageFile], ['Files'])
|
|
282
|
+
// Position that should snap: 137 → 144 (gridSize 24: round(137/24)*24 = 144)
|
|
283
|
+
dropEvent.clientX = 137
|
|
284
|
+
dropEvent.clientY = 85 // 85 → 96
|
|
285
|
+
|
|
286
|
+
dropEvent.preventDefault = vi.fn()
|
|
287
|
+
dropEvent.stopPropagation = vi.fn()
|
|
288
|
+
|
|
289
|
+
scrollContainer.getBoundingClientRect = () => ({ left: 0, top: 0, width: 1000, height: 800 })
|
|
290
|
+
scrollContainer.scrollLeft = 0
|
|
291
|
+
scrollContainer.scrollTop = 0
|
|
292
|
+
|
|
293
|
+
await act(async () => {
|
|
294
|
+
scrollContainer.dispatchEvent(dropEvent)
|
|
295
|
+
await new Promise((r) => setTimeout(r, 50))
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
expect(addWidget).toHaveBeenCalledWith(
|
|
299
|
+
'test-canvas',
|
|
300
|
+
expect.objectContaining({
|
|
301
|
+
position: { x: 144, y: 96 },
|
|
302
|
+
})
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
// Restore
|
|
306
|
+
mockCanvas.snapToGrid = originalSnapToGrid
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
it('does not snap drop position when snap is disabled', async () => {
|
|
310
|
+
// Ensure snap is disabled (default from mock)
|
|
311
|
+
const originalSnapToGrid = mockCanvas.snapToGrid
|
|
312
|
+
mockCanvas.snapToGrid = false
|
|
313
|
+
|
|
314
|
+
render(<CanvasPage canvasId="test-canvas" />)
|
|
315
|
+
const scrollContainer = document.querySelector('[data-storyboard-canvas-scroll]')
|
|
316
|
+
|
|
317
|
+
const imageFile = createMockImageFile()
|
|
318
|
+
const dropEvent = new Event('drop', { bubbles: true, cancelable: true })
|
|
319
|
+
dropEvent.dataTransfer = createDataTransfer([imageFile], ['Files'])
|
|
320
|
+
// Position should round to nearest integer, not snap to grid
|
|
321
|
+
dropEvent.clientX = 137
|
|
322
|
+
dropEvent.clientY = 85
|
|
323
|
+
|
|
324
|
+
dropEvent.preventDefault = vi.fn()
|
|
325
|
+
dropEvent.stopPropagation = vi.fn()
|
|
326
|
+
|
|
327
|
+
scrollContainer.getBoundingClientRect = () => ({ left: 0, top: 0, width: 1000, height: 800 })
|
|
328
|
+
scrollContainer.scrollLeft = 0
|
|
329
|
+
scrollContainer.scrollTop = 0
|
|
330
|
+
|
|
331
|
+
await act(async () => {
|
|
332
|
+
scrollContainer.dispatchEvent(dropEvent)
|
|
333
|
+
await new Promise((r) => setTimeout(r, 50))
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
expect(addWidget).toHaveBeenCalledWith(
|
|
337
|
+
'test-canvas',
|
|
338
|
+
expect.objectContaining({
|
|
339
|
+
position: { x: 137, y: 85 }, // No snapping, just rounded integers
|
|
340
|
+
})
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
// Restore
|
|
344
|
+
mockCanvas.snapToGrid = originalSnapToGrid
|
|
345
|
+
})
|
|
346
|
+
})
|