@dfosco/storyboard-react 2.7.1 → 3.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.
@@ -0,0 +1,134 @@
1
+ import { renderHook } from '@testing-library/react'
2
+ import { createElement } from 'react'
3
+ import { init, getFlowsForPrototype } from '@dfosco/storyboard-core'
4
+ import { useFlows } from './useFlows.js'
5
+ import { StoryboardContext } from '../StoryboardContext.js'
6
+
7
+ // Test data with prototype-scoped flows
8
+ const SCOPED_FLOWS = {
9
+ 'default': { meta: { title: 'Default' } },
10
+ 'Signup/empty-form': { meta: { title: 'Empty Form' }, _route: '/Signup' },
11
+ 'Signup/validation-errors': { meta: { title: 'Validation Errors' }, _route: '/Signup' },
12
+ 'Signup/prefilled-review': { meta: { title: 'Prefilled Review' }, _route: '/Signup' },
13
+ 'Signup/error-state': { meta: { title: 'Error State' }, _route: '/Signup' },
14
+ 'Example/basic': { meta: { title: 'Example Data Flow' }, _route: '/Example' },
15
+ }
16
+
17
+ function seedScopedData() {
18
+ init({ flows: SCOPED_FLOWS, objects: {}, records: {} })
19
+ }
20
+
21
+ function createWrapperWithPrototype(flowName = 'default', prototypeName = null) {
22
+ return function Wrapper({ children }) {
23
+ return createElement(
24
+ StoryboardContext.Provider,
25
+ { value: { data: {}, error: null, loading: false, flowName, sceneName: flowName, prototypeName } },
26
+ children,
27
+ )
28
+ }
29
+ }
30
+
31
+ beforeEach(() => {
32
+ seedScopedData()
33
+ })
34
+
35
+ // ── Core utility: getFlowsForPrototype ──
36
+
37
+ describe('getFlowsForPrototype', () => {
38
+ it('returns flows scoped to the given prototype', () => {
39
+ const flows = getFlowsForPrototype('Signup')
40
+ expect(flows).toHaveLength(4)
41
+ expect(flows.map(f => f.name)).toEqual([
42
+ 'empty-form', 'validation-errors', 'prefilled-review', 'error-state',
43
+ ])
44
+ })
45
+
46
+ it('returns the full key with prototype prefix', () => {
47
+ const flows = getFlowsForPrototype('Signup')
48
+ expect(flows[0].key).toBe('Signup/empty-form')
49
+ })
50
+
51
+ it('returns empty array for prototype with no flows', () => {
52
+ const flows = getFlowsForPrototype('NonExistent')
53
+ expect(flows).toEqual([])
54
+ })
55
+
56
+ it('returns empty array when prototypeName is null', () => {
57
+ const flows = getFlowsForPrototype(null)
58
+ expect(flows).toEqual([])
59
+ })
60
+
61
+ it('returns empty array when prototypeName is empty string', () => {
62
+ const flows = getFlowsForPrototype('')
63
+ expect(flows).toEqual([])
64
+ })
65
+
66
+ it('excludes global flows (no prototype prefix)', () => {
67
+ const flows = getFlowsForPrototype('Signup')
68
+ const keys = flows.map(f => f.key)
69
+ expect(keys).not.toContain('default')
70
+ })
71
+
72
+ it('returns single flow for prototype with one flow', () => {
73
+ const flows = getFlowsForPrototype('Example')
74
+ expect(flows).toHaveLength(1)
75
+ expect(flows[0].name).toBe('basic')
76
+ })
77
+ })
78
+
79
+ // ── useFlows hook ──
80
+
81
+ describe('useFlows', () => {
82
+ it('returns flows for the current prototype', () => {
83
+ const { result } = renderHook(() => useFlows(), {
84
+ wrapper: createWrapperWithPrototype('Signup/empty-form', 'Signup'),
85
+ })
86
+ expect(result.current.flows).toHaveLength(4)
87
+ expect(result.current.flows[0].title).toBe('Empty Form')
88
+ })
89
+
90
+ it('returns the active flow key', () => {
91
+ const { result } = renderHook(() => useFlows(), {
92
+ wrapper: createWrapperWithPrototype('Signup/validation-errors', 'Signup'),
93
+ })
94
+ expect(result.current.activeFlow).toBe('Signup/validation-errors')
95
+ })
96
+
97
+ it('returns prototype name', () => {
98
+ const { result } = renderHook(() => useFlows(), {
99
+ wrapper: createWrapperWithPrototype('Signup/empty-form', 'Signup'),
100
+ })
101
+ expect(result.current.prototypeName).toBe('Signup')
102
+ })
103
+
104
+ it('returns empty flows when no prototype', () => {
105
+ const { result } = renderHook(() => useFlows(), {
106
+ wrapper: createWrapperWithPrototype('default', null),
107
+ })
108
+ expect(result.current.flows).toEqual([])
109
+ })
110
+
111
+ it('switchFlow is a function', () => {
112
+ const { result } = renderHook(() => useFlows(), {
113
+ wrapper: createWrapperWithPrototype('Signup/empty-form', 'Signup'),
114
+ })
115
+ expect(typeof result.current.switchFlow).toBe('function')
116
+ })
117
+
118
+ it('flow entries have title from meta', () => {
119
+ const { result } = renderHook(() => useFlows(), {
120
+ wrapper: createWrapperWithPrototype('Signup/empty-form', 'Signup'),
121
+ })
122
+ const titles = result.current.flows.map(f => f.title)
123
+ expect(titles).toContain('Empty Form')
124
+ expect(titles).toContain('Validation Errors')
125
+ expect(titles).toContain('Prefilled Review')
126
+ expect(titles).toContain('Error State')
127
+ })
128
+
129
+ it('throws when used outside StoryboardProvider', () => {
130
+ expect(() => {
131
+ renderHook(() => useFlows())
132
+ }).toThrow('useFlows must be used within a <StoryboardProvider>')
133
+ })
134
+ })
package/src/index.js CHANGED
@@ -16,6 +16,7 @@ export { useSceneData, useSceneLoading } from './hooks/useSceneData.js'
16
16
  export { useOverride } from './hooks/useOverride.js'
17
17
  export { useOverride as useSession } from './hooks/useOverride.js' // deprecated alias
18
18
  export { useFlow, useScene } from './hooks/useScene.js'
19
+ export { useFlows } from './hooks/useFlows.js'
19
20
  export { useRecord, useRecords } from './hooks/useRecord.js'
20
21
  export { useObject } from './hooks/useObject.js'
21
22
  export { useLocalStorage } from './hooks/useLocalStorage.js'
@@ -35,3 +36,7 @@ export { FormContext } from './context/FormContext.js'
35
36
 
36
37
  // Viewfinder dashboard
37
38
  export { default as Viewfinder } from './Viewfinder.jsx'
39
+
40
+ // Canvas
41
+ export { default as CanvasPage } from './canvas/CanvasPage.jsx'
42
+ export { useCanvas } from './canvas/useCanvas.js'
@@ -3,11 +3,13 @@ import path from 'node:path'
3
3
  import { execSync } from 'node:child_process'
4
4
  import { globSync } from 'glob'
5
5
  import { parse as parseJsonc } from 'jsonc-parser'
6
+ import { materializeFromText } from '../../../core/src/canvas/materializer.js'
6
7
 
7
8
  const VIRTUAL_MODULE_ID = 'virtual:storyboard-data-index'
8
9
  const RESOLVED_ID = '\0' + VIRTUAL_MODULE_ID
9
10
 
10
11
  const GLOB_PATTERN = '**/*.{flow,scene,object,record,prototype,folder}.{json,jsonc}'
12
+ const CANVAS_GLOB_PATTERN = '**/*.canvas.jsonl'
11
13
 
12
14
  /**
13
15
  * Extract the data name and type suffix from a file path.
@@ -22,6 +24,42 @@ const GLOB_PATTERN = '**/*.{flow,scene,object,record,prototype,folder}.{json,jso
22
24
  */
23
25
  function parseDataFile(filePath) {
24
26
  const base = path.basename(filePath)
27
+
28
+ // Handle .canvas.jsonl files
29
+ const canvasJsonlMatch = base.match(/^(.+)\.canvas\.jsonl$/)
30
+ if (canvasJsonlMatch) {
31
+ if (canvasJsonlMatch[1].startsWith('_')) return null
32
+ const normalized = filePath.replace(/\\/g, '/')
33
+ if (normalized.split('/').some(seg => seg.startsWith('_'))) return null
34
+
35
+ const name = canvasJsonlMatch[1]
36
+ let inferredRoute = null
37
+ const canvasFolderMatch = normalized.match(/(?:^|\/)src\/canvases\/([^/]+)\.folder\//)
38
+ const canvasFolderName = canvasFolderMatch ? canvasFolderMatch[1] : null
39
+ const folderDirMatch = normalized.match(/(?:^|\/)src\/prototypes\/([^/]+)\.folder\//)
40
+ const folderName = folderDirMatch ? folderDirMatch[1] : null
41
+
42
+ const canvasCheck = normalized.match(/(?:^|\/)src\/canvases\//)
43
+ if (canvasCheck) {
44
+ const dirPath = normalized.substring(0, normalized.lastIndexOf('/'))
45
+ const routeBase = dirPath
46
+ .replace(/^.*?src\/canvases\//, '')
47
+ .replace(/[^/]*\.folder\/?/g, '')
48
+ inferredRoute = '/canvas/' + (routeBase ? routeBase + '/' : '') + name
49
+ inferredRoute = inferredRoute.replace(/\/+/g, '/').replace(/\/$/, '') || '/canvas'
50
+ }
51
+ const protoCheck = normalized.match(/(?:^|\/)src\/prototypes\//)
52
+ if (!canvasCheck && protoCheck) {
53
+ const dirPath = normalized.substring(0, normalized.lastIndexOf('/'))
54
+ const routeBase = dirPath
55
+ .replace(/^.*?src\/prototypes\//, '')
56
+ .replace(/[^/]*\.folder\/?/g, '')
57
+ inferredRoute = '/canvas/' + (routeBase ? routeBase + '/' : '') + name
58
+ inferredRoute = inferredRoute.replace(/\/+/g, '/').replace(/\/$/, '') || '/canvas'
59
+ }
60
+ return { name, suffix: 'canvas', ext: 'jsonl', folder: canvasFolderName || folderName, inferredRoute }
61
+ }
62
+
25
63
  const match = base.match(/^(.+)\.(flow|scene|object|record|prototype|folder)\.(jsonc?)$/)
26
64
  if (!match) return null
27
65
 
@@ -121,6 +159,7 @@ function getLastModified(root, dirPath) {
121
159
  function buildIndex(root) {
122
160
  const ignore = ['node_modules/**', 'dist/**', '.git/**']
123
161
  const files = globSync(GLOB_PATTERN, { cwd: root, ignore, absolute: false })
162
+ const canvasFiles = globSync(CANVAS_GLOB_PATTERN, { cwd: root, ignore, absolute: false })
124
163
 
125
164
  // Detect nested .folder/ directories (not supported)
126
165
  // Scan directories directly since empty nested folders have no data files
@@ -137,12 +176,13 @@ function buildIndex(root) {
137
176
  }
138
177
  }
139
178
 
140
- const index = { flow: {}, object: {}, record: {}, prototype: {}, folder: {} }
179
+ const index = { flow: {}, object: {}, record: {}, prototype: {}, folder: {}, canvas: {} }
141
180
  const seen = {} // "name.suffix" → absolute path (for duplicate detection)
142
181
  const protoFolders = {} // prototype name → folder name (for injection)
143
182
  const flowRoutes = {} // flow name → inferred route (for _route injection)
183
+ const canvasRoutes = {} // canvas name → inferred route
144
184
 
145
- for (const relPath of files) {
185
+ for (const relPath of [...files, ...canvasFiles]) {
146
186
  const parsed = parseDataFile(relPath)
147
187
  if (!parsed) continue
148
188
 
@@ -175,9 +215,14 @@ function buildIndex(root) {
175
215
  if (parsed.suffix === 'flow' && parsed.inferredRoute) {
176
216
  flowRoutes[parsed.name] = parsed.inferredRoute
177
217
  }
218
+
219
+ // Track inferred routes for canvases
220
+ if (parsed.suffix === 'canvas' && parsed.inferredRoute) {
221
+ canvasRoutes[parsed.name] = parsed.inferredRoute
222
+ }
178
223
  }
179
224
 
180
- return { index, protoFolders, flowRoutes }
225
+ return { index, protoFolders, flowRoutes, canvasRoutes }
181
226
  }
182
227
 
183
228
  /**
@@ -248,25 +293,26 @@ function readConfig(root) {
248
293
  }
249
294
 
250
295
  /**
251
- * Read modes.config.json from @dfosco/storyboard-core.
252
- * Returns the full config object { modes, tools }.
296
+ * Read core-ui.config.json from @dfosco/storyboard-core.
297
+ * Returns the full config object with modes array.
253
298
  * Falls back to hardcoded defaults if not found.
254
299
  */
255
300
  function readModesConfig(root) {
256
301
  const fallback = {
257
302
  modes: [
258
- { name: 'prototype', label: 'Navigate' },
259
- { name: 'inspect', label: 'Develop' },
260
- { name: 'present', label: 'Collaborate' },
261
- { name: 'plan', label: 'Canvas' },
303
+ { name: 'prototype', label: 'Navigate', hue: '#2a2a2a' },
304
+ { name: 'inspect', label: 'Develop', hue: '#7655a4' },
305
+ { name: 'present', label: 'Collaborate', hue: '#2a9d8f' },
306
+ { name: 'plan', label: 'Canvas', hue: '#4a7fad' },
262
307
  ],
263
- tools: {},
264
308
  }
265
309
 
266
310
  // Try local workspace path first (monorepo), then node_modules
267
311
  const candidates = [
268
- path.resolve(root, 'packages/core/modes.config.json'),
269
- path.resolve(root, 'node_modules/@dfosco/storyboard-core/modes.config.json'),
312
+ path.resolve(root, 'packages/core/core-ui.config.json'),
313
+ path.resolve(root, 'packages/core/configs/modes.config.json'),
314
+ path.resolve(root, 'node_modules/@dfosco/storyboard-core/core-ui.config.json'),
315
+ path.resolve(root, 'node_modules/@dfosco/storyboard-core/configs/modes.config.json'),
270
316
  ]
271
317
 
272
318
  for (const filePath of candidates) {
@@ -274,7 +320,7 @@ function readModesConfig(root) {
274
320
  const raw = fs.readFileSync(filePath, 'utf-8')
275
321
  const parsed = JSON.parse(raw)
276
322
  if (Array.isArray(parsed.modes) && parsed.modes.length > 0) {
277
- return { modes: parsed.modes, tools: parsed.tools ?? {} }
323
+ return { modes: parsed.modes }
278
324
  }
279
325
  } catch {
280
326
  // try next candidate
@@ -284,10 +330,10 @@ function readModesConfig(root) {
284
330
  return fallback
285
331
  }
286
332
 
287
- function generateModule({ index, protoFolders, flowRoutes }, root) {
333
+ function generateModule({ index, protoFolders, flowRoutes, canvasRoutes }, root) {
288
334
  const declarations = []
289
- const INDEX_KEYS = ['flow', 'object', 'record', 'prototype', 'folder']
290
- const entries = { flow: [], object: [], record: [], prototype: [], folder: [] }
335
+ const INDEX_KEYS = ['flow', 'object', 'record', 'prototype', 'folder', 'canvas']
336
+ const entries = { flow: [], object: [], record: [], prototype: [], folder: [], canvas: [] }
291
337
  const resolvedFlowRoutes = {} // flow name → resolved route (for multi-flow logging)
292
338
  let i = 0
293
339
 
@@ -295,7 +341,9 @@ function generateModule({ index, protoFolders, flowRoutes }, root) {
295
341
  for (const [name, absPath] of Object.entries(index[suffix])) {
296
342
  const varName = `_d${i++}`
297
343
  const raw = fs.readFileSync(absPath, 'utf-8')
298
- let parsed = parseJsonc(raw)
344
+ let parsed = suffix === 'canvas'
345
+ ? materializeFromText(raw)
346
+ : parseJsonc(raw)
299
347
 
300
348
  // Auto-fill gitAuthor for prototype metadata from git history
301
349
  if (suffix === 'prototype' && parsed && !parsed.gitAuthor) {
@@ -332,6 +380,37 @@ function generateModule({ index, protoFolders, flowRoutes }, root) {
332
380
  }
333
381
  }
334
382
 
383
+ // Inject inferred route and resolve JSX companion for canvases
384
+ if (suffix === 'canvas') {
385
+ if (canvasRoutes[name]) {
386
+ parsed = { ...parsed, _route: canvasRoutes[name] }
387
+ }
388
+ // Inject folder association
389
+ const folderDirMatch = path.relative(root, absPath).replace(/\\/g, '/').match(/(?:^|\/)src\/(?:prototypes|canvases)\/([^/]+)\.folder\//)
390
+ if (folderDirMatch) {
391
+ parsed = { ...parsed, _folder: folderDirMatch[1] }
392
+ }
393
+ // Resolve JSX companion file path
394
+ if (parsed?.jsx) {
395
+ const jsxPath = path.resolve(path.dirname(absPath), parsed.jsx)
396
+ if (fs.existsSync(jsxPath)) {
397
+ const relJsx = '/' + path.relative(root, jsxPath).replace(/\\/g, '/')
398
+ parsed = { ...parsed, _jsxModule: relJsx }
399
+ } else {
400
+ console.warn(
401
+ `[storyboard-data] Canvas "${name}" references JSX file "${parsed.jsx}" but it was not found at ${jsxPath}`
402
+ )
403
+ }
404
+ } else {
405
+ // Auto-detect a same-name .canvas.jsx companion
406
+ const autoJsx = absPath.replace(/\.canvas\.(jsonl|jsonc?)$/, '.canvas.jsx')
407
+ if (fs.existsSync(autoJsx)) {
408
+ const relJsx = '/' + path.relative(root, autoJsx).replace(/\\/g, '/')
409
+ parsed = { ...parsed, _jsxModule: relJsx }
410
+ }
411
+ }
412
+ }
413
+
335
414
  // Resolve template variables (${currentDir}, ${currentProto}, ${currentProtoDir})
336
415
  const templateVars = computeTemplateVars(absPath, root)
337
416
  if (!templateVars.currentProto && raw.includes('${currentProto}')) {
@@ -354,7 +433,7 @@ function generateModule({ index, protoFolders, flowRoutes }, root) {
354
433
  }
355
434
 
356
435
  const imports = [`import { init } from '@dfosco/storyboard-core'`]
357
- const initCalls = [`init({ flows, objects, records, prototypes, folders })`]
436
+ const initCalls = [`init({ flows, objects, records, prototypes, folders, canvases })`]
358
437
 
359
438
  // Feature flags from storyboard.config.json
360
439
  const { config } = readConfig(root)
@@ -383,15 +462,16 @@ function generateModule({ index, protoFolders, flowRoutes }, root) {
383
462
  initCalls.push(`registerMode(${JSON.stringify(m.name)}, { label: ${JSON.stringify(m.label)} })`)
384
463
  }
385
464
 
386
- // Seed tool registry from modes.config.json
387
- if (Object.keys(modesConfig.tools).length > 0) {
388
- initCalls.push(`initTools(${JSON.stringify(modesConfig.tools)})`)
389
- }
390
-
391
465
  initCalls.push(`syncModeClasses()`)
392
466
  }
393
467
  }
394
468
 
469
+ // UI config from storyboard.config.json (menu visibility overrides)
470
+ if (config?.ui) {
471
+ imports.push(`import { initUIConfig } from '@dfosco/storyboard-core'`)
472
+ initCalls.push(`initUIConfig(${JSON.stringify(config.ui)})`)
473
+ }
474
+
395
475
  // Log info when multiple flows target the same route
396
476
  const routeGroups = {}
397
477
  for (const [name, { route, isDefault }] of Object.entries(resolvedFlowRoutes)) {
@@ -422,14 +502,15 @@ function generateModule({ index, protoFolders, flowRoutes }, root) {
422
502
  `const records = {\n${entries.record.join(',\n')}\n}`,
423
503
  `const prototypes = {\n${entries.prototype.join(',\n')}\n}`,
424
504
  `const folders = {\n${entries.folder.join(',\n')}\n}`,
505
+ `const canvases = {\n${entries.canvas.join(',\n')}\n}`,
425
506
  '',
426
507
  '// Backward-compatible alias',
427
508
  'const scenes = flows',
428
509
  '',
429
510
  initCalls.join('\n'),
430
511
  '',
431
- `export { flows, scenes, objects, records, prototypes, folders }`,
432
- `export const index = { flows, scenes, objects, records, prototypes, folders }`,
512
+ `export { flows, scenes, objects, records, prototypes, folders, canvases }`,
513
+ `export const index = { flows, scenes, objects, records, prototypes, folders, canvases }`,
433
514
  `export default index`,
434
515
  ].join('\n')
435
516
  }
@@ -437,7 +518,7 @@ function generateModule({ index, protoFolders, flowRoutes }, root) {
437
518
  /**
438
519
  * Vite plugin for storyboard data discovery.
439
520
  *
440
- * - Scans the repo for *.flow.json, *.scene.json (compat), *.object.json, *.record.json
521
+ * - Scans the repo for *.flow.json, *.scene.json (compat), *.object.json, *.record.json, *.canvas.jsonl
441
522
  * - Validates no two files share the same name+suffix (hard build error)
442
523
  * - Generates a virtual module `virtual:storyboard-data-index`
443
524
  * - Watches for file additions/removals in dev mode
@@ -477,9 +558,15 @@ export default function storyboardDataPlugin() {
477
558
  const watcher = server.watcher
478
559
 
479
560
  const invalidate = (filePath) => {
561
+ const normalized = filePath.replace(/\\/g, '/')
562
+ // Skip .canvas.jsonl content changes entirely — these are mutated
563
+ // at runtime by the canvas server API. A full-reload would create
564
+ // a feedback loop (save → file change → reload → lose editing state).
565
+ if (/\.canvas\.jsonl$/.test(normalized)) return
566
+
480
567
  const parsed = parseDataFile(filePath)
481
568
  // Also invalidate when files are added/removed inside .folder/ directories
482
- const inFolder = filePath.replace(/\\/g, '/').includes('.folder/')
569
+ const inFolder = normalized.includes('.folder/')
483
570
  if (!parsed && !inFolder) return
484
571
  // Rebuild index and invalidate virtual module
485
572
  buildResult = null
@@ -490,6 +577,19 @@ export default function storyboardDataPlugin() {
490
577
  }
491
578
  }
492
579
 
580
+ const invalidateOnAddRemove = (filePath) => {
581
+ const parsed = parseDataFile(filePath)
582
+ const inFolder = filePath.replace(/\\/g, '/').includes('.folder/')
583
+ if (!parsed && !inFolder) return
584
+ // Canvas additions/removals DO need a reload (new routes)
585
+ buildResult = null
586
+ const mod = server.moduleGraph.getModuleById(RESOLVED_ID)
587
+ if (mod) {
588
+ server.moduleGraph.invalidateModule(mod)
589
+ server.ws.send({ type: 'full-reload' })
590
+ }
591
+ }
592
+
493
593
  // Watch storyboard.config.json for changes
494
594
  const { configPath } = readConfig(root)
495
595
  watcher.add(configPath)
@@ -504,8 +604,8 @@ export default function storyboardDataPlugin() {
504
604
  }
505
605
  }
506
606
 
507
- watcher.on('add', invalidate)
508
- watcher.on('unlink', invalidate)
607
+ watcher.on('add', invalidateOnAddRemove)
608
+ watcher.on('unlink', invalidateOnAddRemove)
509
609
  watcher.on('change', (filePath) => {
510
610
  invalidate(filePath)
511
611
  invalidateConfig(filePath)
@@ -69,13 +69,13 @@ describe('storyboardDataPlugin', () => {
69
69
  const code = plugin.load(RESOLVED_ID)
70
70
 
71
71
  expect(code).toContain("import { init } from '@dfosco/storyboard-core'")
72
- expect(code).toContain('init({ flows, objects, records, prototypes, folders })')
72
+ expect(code).toContain('init({ flows, objects, records, prototypes, folders, canvases })')
73
73
  expect(code).toContain('"Test"')
74
74
  expect(code).toContain('"Jane"')
75
75
  expect(code).toContain('"First"')
76
76
  // Backward-compat alias
77
77
  expect(code).toContain('const scenes = flows')
78
- expect(code).toContain('export { flows, scenes, objects, records, prototypes, folders }')
78
+ expect(code).toContain('export { flows, scenes, objects, records, prototypes, folders, canvases }')
79
79
  })
80
80
 
81
81
  it('load returns null for other IDs', () => {
@@ -161,7 +161,7 @@ describe('storyboardDataPlugin', () => {
161
161
 
162
162
  // .scene.json files should be normalized to the flows category
163
163
  expect(code).toContain('"Legacy Scene"')
164
- expect(code).toContain('init({ flows, objects, records, prototypes, folders })')
164
+ expect(code).toContain('init({ flows, objects, records, prototypes, folders, canvases })')
165
165
  })
166
166
 
167
167
  it('buildStart resets the index cache', () => {