@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,474 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
createPasteContext,
|
|
4
|
+
resolvePaste,
|
|
5
|
+
compileRule,
|
|
6
|
+
buildTemplateVars,
|
|
7
|
+
sanitizeUrl,
|
|
8
|
+
resolvePropValue,
|
|
9
|
+
COMPILED_RULES,
|
|
10
|
+
BRANCH_PREFIX_RE,
|
|
11
|
+
} from './pasteRules.js'
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Helpers
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
const ORIGIN = 'https://storyboard.example.com'
|
|
18
|
+
const BASE_PATH = '/storyboard'
|
|
19
|
+
|
|
20
|
+
function ctx(origin = ORIGIN, basePath = BASE_PATH) {
|
|
21
|
+
return createPasteContext(origin, basePath)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// createPasteContext
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
describe('createPasteContext', () => {
|
|
29
|
+
it('stores origin and normalized basePath', () => {
|
|
30
|
+
const c = ctx()
|
|
31
|
+
expect(c.origin).toBe(ORIGIN)
|
|
32
|
+
expect(c.basePath).toBe('/storyboard')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('strips trailing slash from basePath', () => {
|
|
36
|
+
const c = createPasteContext(ORIGIN, '/storyboard/')
|
|
37
|
+
expect(c.basePath).toBe('/storyboard')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
describe('isSameOrigin', () => {
|
|
41
|
+
it('matches exact base URL', () => {
|
|
42
|
+
expect(ctx().isSameOrigin(`${ORIGIN}/storyboard`)).toBe(true)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('matches sub-path under base', () => {
|
|
46
|
+
expect(ctx().isSameOrigin(`${ORIGIN}/storyboard/MyProto`)).toBe(true)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('rejects different origin', () => {
|
|
50
|
+
expect(ctx().isSameOrigin('https://evil.com/storyboard/foo')).toBe(false)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('rejects spoofed host with matching prefix', () => {
|
|
54
|
+
expect(ctx().isSameOrigin('https://storyboard.example.com.evil.com/storyboard/x')).toBe(false)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('rejects basePath prefix collision (/storyboard vs /storyboard-beta)', () => {
|
|
58
|
+
expect(ctx().isSameOrigin(`${ORIGIN}/storyboard-beta/foo`)).toBe(false)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('matches branch deploy URL', () => {
|
|
62
|
+
expect(ctx().isSameOrigin(`${ORIGIN}/branch--my-feature/MyProto`)).toBe(true)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('rejects non-http protocols', () => {
|
|
66
|
+
expect(ctx().isSameOrigin('ftp://storyboard.example.com/storyboard/x')).toBe(false)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('handles root basePath', () => {
|
|
70
|
+
const c = createPasteContext(ORIGIN, '/')
|
|
71
|
+
expect(c.isSameOrigin(`${ORIGIN}/anything`)).toBe(true)
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
describe('extractSrc', () => {
|
|
76
|
+
it('strips base path', () => {
|
|
77
|
+
expect(ctx().extractSrc('/storyboard/MyProto')).toBe('/MyProto')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('strips branch prefix', () => {
|
|
81
|
+
expect(ctx().extractSrc('/branch--feat/MyProto')).toBe('/MyProto')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('returns / for base path alone', () => {
|
|
85
|
+
expect(ctx().extractSrc('/storyboard')).toBe('/')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('returns pathname as-is when no prefix matches', () => {
|
|
89
|
+
expect(ctx().extractSrc('/other/path')).toBe('/other/path')
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
describe('parseUrl', () => {
|
|
94
|
+
it('parses http URL', () => {
|
|
95
|
+
const u = ctx().parseUrl('https://example.com/path')
|
|
96
|
+
expect(u).not.toBeNull()
|
|
97
|
+
expect(u.hostname).toBe('example.com')
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('returns null for non-http', () => {
|
|
101
|
+
expect(ctx().parseUrl('ftp://example.com')).toBeNull()
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('returns null for invalid URL', () => {
|
|
105
|
+
expect(ctx().parseUrl('not a url')).toBeNull()
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('returns null for empty string', () => {
|
|
109
|
+
expect(ctx().parseUrl('')).toBeNull()
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// BRANCH_PREFIX_RE
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
describe('BRANCH_PREFIX_RE', () => {
|
|
119
|
+
it('matches /branch--name', () => {
|
|
120
|
+
expect(BRANCH_PREFIX_RE.test('/branch--my-feature')).toBe(true)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('matches /branch--name/rest', () => {
|
|
124
|
+
expect(BRANCH_PREFIX_RE.test('/branch--fix/Proto/page')).toBe(true)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('does not match /branching', () => {
|
|
128
|
+
expect(BRANCH_PREFIX_RE.test('/branching')).toBe(false)
|
|
129
|
+
})
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// sanitizeUrl
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
describe('sanitizeUrl', () => {
|
|
137
|
+
it('strips specified params', () => {
|
|
138
|
+
const result = sanitizeUrl('https://figma.com/board/abc/Name?t=token&node-id=1', { stripParams: ['t'] })
|
|
139
|
+
expect(result).not.toContain('t=token')
|
|
140
|
+
expect(result).toContain('node-id=1')
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('normalizes hostname', () => {
|
|
144
|
+
const result = sanitizeUrl('https://figma.com/board/abc/Name', { normalizeHost: 'www.figma.com' })
|
|
145
|
+
expect(result).toContain('www.figma.com')
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('returns original on invalid URL', () => {
|
|
149
|
+
expect(sanitizeUrl('not-a-url', { stripParams: ['t'] })).toBe('not-a-url')
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// buildTemplateVars
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
describe('buildTemplateVars', () => {
|
|
158
|
+
const c = ctx()
|
|
159
|
+
|
|
160
|
+
it('builds all vars from a parsed URL', () => {
|
|
161
|
+
const parsed = new URL(`${ORIGIN}/storyboard/MyProto?flow=alt#over`)
|
|
162
|
+
const vars = buildTemplateVars(parsed.toString(), parsed, c)
|
|
163
|
+
expect(vars.$url).toBe(parsed.toString())
|
|
164
|
+
expect(vars.$text).toBe(parsed.toString())
|
|
165
|
+
expect(vars.$pathname).toBe('/storyboard/MyProto')
|
|
166
|
+
expect(vars.$src).toBe('/MyProto')
|
|
167
|
+
expect(vars.$search).toBe('?flow=alt')
|
|
168
|
+
expect(vars.$hash).toBe('#over')
|
|
169
|
+
expect(vars.$hostname).toBe('storyboard.example.com')
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('handles null parsed URL (plain text)', () => {
|
|
173
|
+
const vars = buildTemplateVars('hello world', null, c)
|
|
174
|
+
expect(vars.$url).toBe('hello world')
|
|
175
|
+
expect(vars.$text).toBe('hello world')
|
|
176
|
+
expect(vars.$pathname).toBe('')
|
|
177
|
+
expect(vars.$src).toBe('')
|
|
178
|
+
expect(vars.$hostname).toBe('')
|
|
179
|
+
})
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
// resolvePropValue
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
describe('resolvePropValue', () => {
|
|
187
|
+
const vars = { $url: 'https://example.com', $text: 'https://example.com', $src: '/MyProto', $pathname: '/storyboard/MyProto', $search: '', $hash: '', $hostname: 'example.com', $origin: 'https://example.com' }
|
|
188
|
+
|
|
189
|
+
it('resolves template object', () => {
|
|
190
|
+
expect(resolvePropValue({ template: '$src' }, vars)).toBe('/MyProto')
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('resolves template with sanitize', () => {
|
|
194
|
+
const result = resolvePropValue(
|
|
195
|
+
{ template: '$url', sanitize: { stripParams: ['t'], normalizeHost: 'www.figma.com' } },
|
|
196
|
+
{ ...vars, $url: 'https://figma.com/board/abc?t=token' }
|
|
197
|
+
)
|
|
198
|
+
expect(result).toContain('www.figma.com')
|
|
199
|
+
expect(result).not.toContain('t=token')
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it('resolves plain string with template vars', () => {
|
|
203
|
+
expect(resolvePropValue('path is $pathname', vars)).toBe('path is /storyboard/MyProto')
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it('passes through numbers', () => {
|
|
207
|
+
expect(resolvePropValue(800, vars)).toBe(800)
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it('passes through null', () => {
|
|
211
|
+
expect(resolvePropValue(null, vars)).toBeNull()
|
|
212
|
+
})
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
// compileRule
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
describe('compileRule', () => {
|
|
220
|
+
const c = ctx()
|
|
221
|
+
|
|
222
|
+
it('compiles hostname + pathname matcher', () => {
|
|
223
|
+
const rule = compileRule({
|
|
224
|
+
name: 'figma',
|
|
225
|
+
match: { hostname: '^(www\\.)?figma\\.com$', pathname: '^/(board|design|proto)/' },
|
|
226
|
+
widget: 'figma-embed',
|
|
227
|
+
props: { url: { template: '$url' }, width: 800 },
|
|
228
|
+
})
|
|
229
|
+
expect(rule).not.toBeNull()
|
|
230
|
+
const parsed = c.parseUrl('https://www.figma.com/board/abc/Name')
|
|
231
|
+
expect(rule.match('https://www.figma.com/board/abc/Name', parsed, c)).toBe(true)
|
|
232
|
+
const nonFigma = c.parseUrl('https://github.com/repo')
|
|
233
|
+
expect(rule.match('https://github.com/repo', nonFigma, c)).toBe(false)
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
it('compiles sameOrigin matcher', () => {
|
|
237
|
+
const rule = compileRule({
|
|
238
|
+
name: 'proto',
|
|
239
|
+
match: { sameOrigin: true },
|
|
240
|
+
widget: 'prototype',
|
|
241
|
+
props: { src: { template: '$src' } },
|
|
242
|
+
})
|
|
243
|
+
const parsed = c.parseUrl(`${ORIGIN}/storyboard/MyProto`)
|
|
244
|
+
expect(rule.match(`${ORIGIN}/storyboard/MyProto`, parsed, c)).toBe(true)
|
|
245
|
+
const ext = c.parseUrl('https://other.com/page')
|
|
246
|
+
expect(rule.match('https://other.com/page', ext, c)).toBe(false)
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
it('compiles isUrl matcher', () => {
|
|
250
|
+
const rule = compileRule({
|
|
251
|
+
name: 'link',
|
|
252
|
+
match: { isUrl: true },
|
|
253
|
+
widget: 'link-preview',
|
|
254
|
+
props: { url: { template: '$url' } },
|
|
255
|
+
})
|
|
256
|
+
const parsed = c.parseUrl('https://example.com')
|
|
257
|
+
expect(rule.match('https://example.com', parsed, c)).toBe(true)
|
|
258
|
+
expect(rule.match('plain text', null, c)).toBe(false)
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
it('compiles any matcher', () => {
|
|
262
|
+
const rule = compileRule({
|
|
263
|
+
name: 'fallback',
|
|
264
|
+
match: { any: true },
|
|
265
|
+
widget: 'markdown',
|
|
266
|
+
props: { content: { template: '$text' } },
|
|
267
|
+
})
|
|
268
|
+
expect(rule.match('anything', null, c)).toBe(true)
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
it('compiles pattern matcher', () => {
|
|
272
|
+
const rule = compileRule({
|
|
273
|
+
name: 'youtube',
|
|
274
|
+
match: { pattern: 'youtube\\.com/watch' },
|
|
275
|
+
widget: 'link-preview',
|
|
276
|
+
props: { url: { template: '$url' } },
|
|
277
|
+
})
|
|
278
|
+
expect(rule.match('https://youtube.com/watch?v=abc', null, c)).toBe(true)
|
|
279
|
+
expect(rule.match('https://vimeo.com/123', null, c)).toBe(false)
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
it('returns null for missing match', () => {
|
|
283
|
+
expect(compileRule({ widget: 'test' })).toBeNull()
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
it('returns null for missing widget', () => {
|
|
287
|
+
expect(compileRule({ match: { any: true } })).toBeNull()
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
it('returns null for invalid regex', () => {
|
|
291
|
+
const spy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
292
|
+
expect(compileRule({ name: 'bad', match: { hostname: '[invalid' }, widget: 'test' })).toBeNull()
|
|
293
|
+
spy.mockRestore()
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
it('resolves props with template vars', () => {
|
|
297
|
+
const rule = compileRule({
|
|
298
|
+
name: 'test',
|
|
299
|
+
match: { any: true },
|
|
300
|
+
widget: 'markdown',
|
|
301
|
+
props: { content: { template: '$text' }, width: 400 },
|
|
302
|
+
})
|
|
303
|
+
const result = rule.resolve('hello', null, c)
|
|
304
|
+
expect(result.type).toBe('markdown')
|
|
305
|
+
expect(result.props.content).toBe('hello')
|
|
306
|
+
expect(result.props.width).toBe(400)
|
|
307
|
+
})
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
// ---------------------------------------------------------------------------
|
|
311
|
+
// COMPILED_RULES (from paste.config.json)
|
|
312
|
+
// ---------------------------------------------------------------------------
|
|
313
|
+
|
|
314
|
+
describe('COMPILED_RULES', () => {
|
|
315
|
+
it('compiles all rules from paste.config.json', () => {
|
|
316
|
+
expect(COMPILED_RULES.length).toBeGreaterThanOrEqual(4)
|
|
317
|
+
const names = COMPILED_RULES.map(r => r.name)
|
|
318
|
+
expect(names).toContain('figma')
|
|
319
|
+
expect(names).toContain('same-origin')
|
|
320
|
+
expect(names).toContain('link-preview')
|
|
321
|
+
expect(names).toContain('markdown')
|
|
322
|
+
})
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
// ---------------------------------------------------------------------------
|
|
326
|
+
// resolvePaste — end-to-end with paste.config.json rules
|
|
327
|
+
// ---------------------------------------------------------------------------
|
|
328
|
+
|
|
329
|
+
describe('resolvePaste', () => {
|
|
330
|
+
const c = ctx()
|
|
331
|
+
|
|
332
|
+
describe('figma rule', () => {
|
|
333
|
+
it('creates figma-embed for figma board URL', () => {
|
|
334
|
+
const text = 'https://www.figma.com/board/abc123/My-Board'
|
|
335
|
+
const result = resolvePaste(text, c)
|
|
336
|
+
expect(result.type).toBe('figma-embed')
|
|
337
|
+
expect(result.props.url).toContain('figma.com')
|
|
338
|
+
expect(result.props.width).toBe(800)
|
|
339
|
+
expect(result.props.height).toBe(450)
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
it('sanitizes figma URL (strips tracking params)', () => {
|
|
343
|
+
const text = 'https://figma.com/design/abc/Name?t=trackingToken'
|
|
344
|
+
const result = resolvePaste(text, c)
|
|
345
|
+
expect(result.type).toBe('figma-embed')
|
|
346
|
+
expect(result.props.url).not.toContain('trackingToken')
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
it('normalizes figma hostname to www.figma.com', () => {
|
|
350
|
+
const text = 'https://figma.com/board/abc/Name'
|
|
351
|
+
const result = resolvePaste(text, c)
|
|
352
|
+
expect(result.props.url).toContain('www.figma.com')
|
|
353
|
+
})
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
describe('same-origin rule', () => {
|
|
357
|
+
it('creates prototype widget for same-origin URL', () => {
|
|
358
|
+
const text = `${ORIGIN}/storyboard/MyProto`
|
|
359
|
+
const result = resolvePaste(text, c)
|
|
360
|
+
expect(result.type).toBe('prototype')
|
|
361
|
+
expect(result.props.src).toBe('/MyProto')
|
|
362
|
+
expect(result.props.originalSrc).toBe('/MyProto')
|
|
363
|
+
expect(result.props.width).toBe(800)
|
|
364
|
+
expect(result.props.height).toBe(600)
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
it('creates prototype widget for branch deploy URL', () => {
|
|
368
|
+
const text = `${ORIGIN}/branch--feat/MyProto`
|
|
369
|
+
const result = resolvePaste(text, c)
|
|
370
|
+
expect(result.type).toBe('prototype')
|
|
371
|
+
expect(result.props.src).toBe('/MyProto')
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
it('preserves search and hash in src', () => {
|
|
375
|
+
const text = `${ORIGIN}/storyboard/Proto?flow=alt#override`
|
|
376
|
+
const result = resolvePaste(text, c)
|
|
377
|
+
expect(result.type).toBe('prototype')
|
|
378
|
+
expect(result.props.src).toBe('/Proto?flow=alt#override')
|
|
379
|
+
})
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
describe('link-preview rule', () => {
|
|
383
|
+
it('creates link-preview for external URL', () => {
|
|
384
|
+
const text = 'https://github.com/dfosco/storyboard'
|
|
385
|
+
const result = resolvePaste(text, c)
|
|
386
|
+
expect(result.type).toBe('link-preview')
|
|
387
|
+
expect(result.props.url).toBe(text)
|
|
388
|
+
expect(result.props.title).toBe('')
|
|
389
|
+
})
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
describe('markdown rule (fallback)', () => {
|
|
393
|
+
it('creates markdown widget for plain text', () => {
|
|
394
|
+
const result = resolvePaste('Hello world', c)
|
|
395
|
+
expect(result.type).toBe('markdown')
|
|
396
|
+
expect(result.props.content).toBe('Hello world')
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
it('creates markdown for non-URL text with slashes', () => {
|
|
400
|
+
const result = resolvePaste('some/path/thing', c)
|
|
401
|
+
expect(result.type).toBe('markdown')
|
|
402
|
+
})
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
describe('rule precedence', () => {
|
|
406
|
+
it('figma wins over same-origin (if origin were figma.com)', () => {
|
|
407
|
+
const figmaCtx = createPasteContext('https://www.figma.com', '/')
|
|
408
|
+
const text = 'https://www.figma.com/board/abc/Name'
|
|
409
|
+
const result = resolvePaste(text, figmaCtx)
|
|
410
|
+
expect(result.type).toBe('figma-embed')
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
it('same-origin wins over generic link-preview', () => {
|
|
414
|
+
const text = `${ORIGIN}/storyboard/Proto`
|
|
415
|
+
const result = resolvePaste(text, c)
|
|
416
|
+
expect(result.type).toBe('prototype')
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
it('link-preview wins over markdown for URLs', () => {
|
|
420
|
+
const text = 'https://example.com'
|
|
421
|
+
const result = resolvePaste(text, c)
|
|
422
|
+
expect(result.type).toBe('link-preview')
|
|
423
|
+
})
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
describe('edge cases', () => {
|
|
427
|
+
it('handles empty string gracefully (markdown)', () => {
|
|
428
|
+
const result = resolvePaste('', c)
|
|
429
|
+
expect(result.type).toBe('markdown')
|
|
430
|
+
expect(result.props.content).toBe('')
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
it('handles malformed URL-like text without crashing', () => {
|
|
434
|
+
const result = resolvePaste('http://', c)
|
|
435
|
+
expect(result).not.toBeNull()
|
|
436
|
+
})
|
|
437
|
+
})
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
// ---------------------------------------------------------------------------
|
|
441
|
+
// Override rules (from storyboard.config.json canvas.pasteRules)
|
|
442
|
+
// ---------------------------------------------------------------------------
|
|
443
|
+
|
|
444
|
+
describe('resolvePaste with override rules', () => {
|
|
445
|
+
const c = ctx()
|
|
446
|
+
|
|
447
|
+
it('override rule takes priority over paste.config.json rules', () => {
|
|
448
|
+
const overrides = [
|
|
449
|
+
{ name: 'custom-github', match: { pattern: 'github\\.com' }, widget: 'markdown', props: { content: 'GitHub link: $url' } },
|
|
450
|
+
]
|
|
451
|
+
const result = resolvePaste('https://github.com/repo', c, overrides)
|
|
452
|
+
expect(result.type).toBe('markdown')
|
|
453
|
+
expect(result.props.content).toBe('GitHub link: https://github.com/repo')
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
it('falls through to paste.config.json rules when override does not match', () => {
|
|
457
|
+
const overrides = [
|
|
458
|
+
{ name: 'youtube', match: { pattern: 'youtube\\.com' }, widget: 'video', props: {} },
|
|
459
|
+
]
|
|
460
|
+
const result = resolvePaste('https://github.com/repo', c, overrides)
|
|
461
|
+
expect(result.type).toBe('link-preview')
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
it('invalid override rules are silently skipped', () => {
|
|
465
|
+
const spy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
466
|
+
const overrides = [
|
|
467
|
+
{ name: 'bad', match: { hostname: '[invalid' }, widget: 'test' },
|
|
468
|
+
{ name: 'good', match: { pattern: 'github\\.com' }, widget: 'custom', props: { url: { template: '$url' } } },
|
|
469
|
+
]
|
|
470
|
+
const result = resolvePaste('https://github.com/repo', c, overrides)
|
|
471
|
+
expect(result.type).toBe('custom')
|
|
472
|
+
spy.mockRestore()
|
|
473
|
+
})
|
|
474
|
+
})
|