@dfosco/storyboard-core 2.0.0 → 2.2.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/modes.config.json +17 -0
- package/package.json +8 -3
- package/src/bodyClasses.js +11 -8
- package/src/bodyClasses.test.js +26 -12
- package/src/devtools.js +9 -8
- package/src/devtools.test.js +2 -2
- package/src/index.js +20 -5
- package/src/loader.js +116 -35
- package/src/loader.test.js +189 -48
- package/src/modes.js +170 -0
- package/src/modes.test.js +216 -0
- package/src/sceneDebug.js +17 -13
- package/src/sceneDebug.test.js +42 -29
- package/src/svelte-plugin-ui/__tests__/ToolbarShell.test.ts +77 -19
- package/src/svelte-plugin-ui/__tests__/designModes.test.ts +1 -1
- package/src/svelte-plugin-ui/components/ToolbarShell.svelte +59 -19
- package/src/svelte-plugin-ui/components/Viewfinder.svelte +573 -0
- package/src/svelte-plugin-ui/index.ts +3 -0
- package/src/svelte-plugin-ui/stores/toolStore.ts +71 -0
- package/src/svelte-plugin-ui/stores/types.ts +22 -8
- package/src/{svelte-plugin-ui/plugins → ui}/design-modes.ts +9 -7
- package/src/ui/viewfinder.ts +58 -0
- package/src/viewfinder.js +100 -20
- package/src/viewfinder.test.js +64 -42
- package/src/workshop/features/createPage/server.js +8 -8
package/src/modes.test.js
CHANGED
|
@@ -10,6 +10,13 @@ import {
|
|
|
10
10
|
on,
|
|
11
11
|
off,
|
|
12
12
|
emit,
|
|
13
|
+
initTools,
|
|
14
|
+
setToolAction,
|
|
15
|
+
setToolState,
|
|
16
|
+
getToolState,
|
|
17
|
+
getToolsForMode,
|
|
18
|
+
subscribeToTools,
|
|
19
|
+
getToolsSnapshot,
|
|
13
20
|
_resetModes,
|
|
14
21
|
} from './modes.js'
|
|
15
22
|
|
|
@@ -289,3 +296,212 @@ describe('modes config', () => {
|
|
|
289
296
|
expect(isModesEnabled()).toBe(false)
|
|
290
297
|
})
|
|
291
298
|
})
|
|
299
|
+
|
|
300
|
+
// ---------------------------------------------------------------------------
|
|
301
|
+
// Tool registry
|
|
302
|
+
// ---------------------------------------------------------------------------
|
|
303
|
+
|
|
304
|
+
describe('tool registry', () => {
|
|
305
|
+
const SAMPLE_TOOLS = {
|
|
306
|
+
'*': [
|
|
307
|
+
{ id: 'viewfinder', label: 'Viewfinder', group: 'dev' },
|
|
308
|
+
{ id: 'reset-params', label: 'Reset all params', group: 'dev' },
|
|
309
|
+
],
|
|
310
|
+
'present': [
|
|
311
|
+
{ id: 'comments-toggle', label: 'Comments', group: 'tools' },
|
|
312
|
+
],
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
describe('initTools', () => {
|
|
316
|
+
it('seeds the registry from config', () => {
|
|
317
|
+
initTools(SAMPLE_TOOLS)
|
|
318
|
+
const tools = getToolsForMode('present')
|
|
319
|
+
const ids = tools.map(t => t.id)
|
|
320
|
+
expect(ids).toContain('viewfinder')
|
|
321
|
+
expect(ids).toContain('comments-toggle')
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
it('wildcard tools appear in all modes', () => {
|
|
325
|
+
initTools(SAMPLE_TOOLS)
|
|
326
|
+
const protoTools = getToolsForMode('prototype')
|
|
327
|
+
expect(protoTools.map(t => t.id)).toContain('viewfinder')
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
it('mode-specific tools only appear in their mode', () => {
|
|
331
|
+
initTools(SAMPLE_TOOLS)
|
|
332
|
+
const protoTools = getToolsForMode('prototype')
|
|
333
|
+
expect(protoTools.map(t => t.id)).not.toContain('comments-toggle')
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
it('tools start with default state', () => {
|
|
337
|
+
initTools(SAMPLE_TOOLS)
|
|
338
|
+
const state = getToolState('viewfinder')
|
|
339
|
+
expect(state).toEqual({
|
|
340
|
+
enabled: true,
|
|
341
|
+
active: false,
|
|
342
|
+
busy: false,
|
|
343
|
+
hidden: false,
|
|
344
|
+
badge: null,
|
|
345
|
+
})
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
it('merges mode assignments when tool appears in multiple keys', () => {
|
|
349
|
+
initTools({
|
|
350
|
+
'*': [{ id: 'shared', label: 'Shared', group: 'dev' }],
|
|
351
|
+
'inspect': [{ id: 'shared', label: 'Shared', group: 'dev' }],
|
|
352
|
+
})
|
|
353
|
+
const tool = getToolsForMode('inspect').find(t => t.id === 'shared')
|
|
354
|
+
expect(tool.modes).toContain('*')
|
|
355
|
+
expect(tool.modes).toContain('inspect')
|
|
356
|
+
})
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
describe('setToolAction', () => {
|
|
360
|
+
it('wires up an action callback', () => {
|
|
361
|
+
initTools({ '*': [{ id: 'test-tool', label: 'Test', group: 'tools' }] })
|
|
362
|
+
const action = vi.fn()
|
|
363
|
+
setToolAction('test-tool', action)
|
|
364
|
+
|
|
365
|
+
const tool = getToolsForMode('prototype').find(t => t.id === 'test-tool')
|
|
366
|
+
expect(tool.action).toBe(action)
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
it('warns when tool is not declared', () => {
|
|
370
|
+
const spy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
371
|
+
setToolAction('nonexistent', () => {})
|
|
372
|
+
expect(spy).toHaveBeenCalledWith(expect.stringContaining('not declared'))
|
|
373
|
+
spy.mockRestore()
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
it('notifies subscribers', () => {
|
|
377
|
+
initTools({ '*': [{ id: 'tool-a', label: 'A', group: 'tools' }] })
|
|
378
|
+
const cb = vi.fn()
|
|
379
|
+
const unsub = subscribeToTools(cb)
|
|
380
|
+
setToolAction('tool-a', () => {})
|
|
381
|
+
expect(cb).toHaveBeenCalled()
|
|
382
|
+
unsub()
|
|
383
|
+
})
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
describe('setToolState', () => {
|
|
387
|
+
it('merges partial state updates', () => {
|
|
388
|
+
initTools({ '*': [{ id: 'tool-b', label: 'B', group: 'tools' }] })
|
|
389
|
+
setToolState('tool-b', { active: true })
|
|
390
|
+
expect(getToolState('tool-b').active).toBe(true)
|
|
391
|
+
expect(getToolState('tool-b').enabled).toBe(true) // unchanged
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
it('sets busy state', () => {
|
|
395
|
+
initTools({ '*': [{ id: 'tool-c', label: 'C', group: 'tools' }] })
|
|
396
|
+
setToolState('tool-c', { busy: true })
|
|
397
|
+
expect(getToolState('tool-c').busy).toBe(true)
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
it('sets badge', () => {
|
|
401
|
+
initTools({ '*': [{ id: 'tool-d', label: 'D', group: 'tools' }] })
|
|
402
|
+
setToolState('tool-d', { badge: 3 })
|
|
403
|
+
expect(getToolState('tool-d').badge).toBe(3)
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
it('warns when tool is not declared', () => {
|
|
407
|
+
const spy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
408
|
+
setToolState('nonexistent', { enabled: false })
|
|
409
|
+
expect(spy).toHaveBeenCalledWith(expect.stringContaining('not declared'))
|
|
410
|
+
spy.mockRestore()
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
it('notifies subscribers', () => {
|
|
414
|
+
initTools({ '*': [{ id: 'tool-e', label: 'E', group: 'tools' }] })
|
|
415
|
+
const cb = vi.fn()
|
|
416
|
+
const unsub = subscribeToTools(cb)
|
|
417
|
+
setToolState('tool-e', { active: true })
|
|
418
|
+
expect(cb).toHaveBeenCalled()
|
|
419
|
+
unsub()
|
|
420
|
+
})
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
describe('getToolsForMode', () => {
|
|
424
|
+
it('sorts tools group first, dev second', () => {
|
|
425
|
+
initTools({
|
|
426
|
+
'*': [
|
|
427
|
+
{ id: 'dev-tool', label: 'Dev', group: 'dev' },
|
|
428
|
+
{ id: 'main-tool', label: 'Main', group: 'tools' },
|
|
429
|
+
],
|
|
430
|
+
})
|
|
431
|
+
const tools = getToolsForMode('prototype')
|
|
432
|
+
expect(tools[0].id).toBe('main-tool')
|
|
433
|
+
expect(tools[1].id).toBe('dev-tool')
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
it('sorts by order within a group', () => {
|
|
437
|
+
initTools({
|
|
438
|
+
'*': [
|
|
439
|
+
{ id: 'b', label: 'B', group: 'tools', order: 20 },
|
|
440
|
+
{ id: 'a', label: 'A', group: 'tools', order: 10 },
|
|
441
|
+
],
|
|
442
|
+
})
|
|
443
|
+
const tools = getToolsForMode('prototype')
|
|
444
|
+
expect(tools[0].id).toBe('a')
|
|
445
|
+
expect(tools[1].id).toBe('b')
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
it('excludes hidden tools', () => {
|
|
449
|
+
initTools({ '*': [{ id: 'hidden-tool', label: 'Hidden', group: 'tools' }] })
|
|
450
|
+
setToolState('hidden-tool', { hidden: true })
|
|
451
|
+
const tools = getToolsForMode('prototype')
|
|
452
|
+
expect(tools.map(t => t.id)).not.toContain('hidden-tool')
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
it('includes state and action in returned tools', () => {
|
|
456
|
+
initTools({ '*': [{ id: 'full-tool', label: 'Full', group: 'tools' }] })
|
|
457
|
+
const action = vi.fn()
|
|
458
|
+
setToolAction('full-tool', action)
|
|
459
|
+
setToolState('full-tool', { active: true, badge: 5 })
|
|
460
|
+
|
|
461
|
+
const tool = getToolsForMode('prototype').find(t => t.id === 'full-tool')
|
|
462
|
+
expect(tool.state.active).toBe(true)
|
|
463
|
+
expect(tool.state.badge).toBe(5)
|
|
464
|
+
expect(tool.action).toBe(action)
|
|
465
|
+
})
|
|
466
|
+
|
|
467
|
+
it('returns null action when not wired up', () => {
|
|
468
|
+
initTools({ '*': [{ id: 'no-action', label: 'No Action', group: 'tools' }] })
|
|
469
|
+
const tool = getToolsForMode('prototype').find(t => t.id === 'no-action')
|
|
470
|
+
expect(tool.action).toBeNull()
|
|
471
|
+
})
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
describe('subscribeToTools', () => {
|
|
475
|
+
it('unsubscribe stops further calls', () => {
|
|
476
|
+
const cb = vi.fn()
|
|
477
|
+
const unsub = subscribeToTools(cb)
|
|
478
|
+
unsub()
|
|
479
|
+
initTools({ '*': [{ id: 'x', label: 'X', group: 'tools' }] })
|
|
480
|
+
expect(cb).not.toHaveBeenCalled()
|
|
481
|
+
})
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
describe('getToolsSnapshot', () => {
|
|
485
|
+
it('changes when state changes', () => {
|
|
486
|
+
initTools({ '*': [{ id: 'snap-tool', label: 'Snap', group: 'tools' }] })
|
|
487
|
+
const snap1 = getToolsSnapshot()
|
|
488
|
+
setToolState('snap-tool', { active: true })
|
|
489
|
+
const snap2 = getToolsSnapshot()
|
|
490
|
+
expect(snap1).not.toBe(snap2)
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
it('changes when action is set', () => {
|
|
494
|
+
initTools({ '*': [{ id: 'snap-tool2', label: 'Snap2', group: 'tools' }] })
|
|
495
|
+
const snap1 = getToolsSnapshot()
|
|
496
|
+
setToolAction('snap-tool2', () => {})
|
|
497
|
+
const snap2 = getToolsSnapshot()
|
|
498
|
+
expect(snap1).not.toBe(snap2)
|
|
499
|
+
})
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
describe('getToolState', () => {
|
|
503
|
+
it('returns null for undeclared tools', () => {
|
|
504
|
+
expect(getToolState('nonexistent')).toBeNull()
|
|
505
|
+
})
|
|
506
|
+
})
|
|
507
|
+
})
|
package/src/sceneDebug.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Storyboard
|
|
2
|
+
* Storyboard FlowDebug — a vanilla JS debug panel that displays flow data.
|
|
3
3
|
*
|
|
4
4
|
* Framework-agnostic: creates a DOM element, no React/Vue/etc. needed.
|
|
5
5
|
*
|
|
6
6
|
* Usage:
|
|
7
|
-
* import {
|
|
8
|
-
*
|
|
7
|
+
* import { mountFlowDebug } from '@dfosco/storyboard-core'
|
|
8
|
+
* mountFlowDebug(document.getElementById('debug'))
|
|
9
9
|
* // or
|
|
10
|
-
*
|
|
10
|
+
* mountFlowDebug() // appends to document.body
|
|
11
11
|
*/
|
|
12
|
-
import {
|
|
12
|
+
import { loadFlow } from './loader.js'
|
|
13
13
|
|
|
14
14
|
const STYLES = `
|
|
15
15
|
.sb-scene-debug {
|
|
@@ -53,16 +53,17 @@ const STYLES = `
|
|
|
53
53
|
let stylesInjected = false
|
|
54
54
|
|
|
55
55
|
/**
|
|
56
|
-
* Mount a
|
|
56
|
+
* Mount a flow debug panel into the DOM.
|
|
57
57
|
*
|
|
58
58
|
* @param {HTMLElement} [container=document.body] - Where to mount
|
|
59
|
-
* @param {string} [
|
|
59
|
+
* @param {string} [flowName] - Flow name override (defaults to ?flow= param or "default")
|
|
60
60
|
* @returns {HTMLElement} The created debug element
|
|
61
61
|
*/
|
|
62
|
-
export function
|
|
62
|
+
export function mountFlowDebug(container, flowName) {
|
|
63
63
|
const target = container || document.body
|
|
64
|
-
const
|
|
65
|
-
|
|
64
|
+
const sp = new URLSearchParams(window.location.search)
|
|
65
|
+
const activeFlowName = flowName
|
|
66
|
+
|| sp.get('flow') || sp.get('scene')
|
|
66
67
|
|| 'default'
|
|
67
68
|
|
|
68
69
|
// Inject styles once
|
|
@@ -79,7 +80,7 @@ export function mountSceneDebug(container, sceneName) {
|
|
|
79
80
|
let data = null
|
|
80
81
|
let error = null
|
|
81
82
|
try {
|
|
82
|
-
data =
|
|
83
|
+
data = loadFlow(activeFlowName)
|
|
83
84
|
} catch (err) {
|
|
84
85
|
error = err.message
|
|
85
86
|
}
|
|
@@ -87,13 +88,13 @@ export function mountSceneDebug(container, sceneName) {
|
|
|
87
88
|
if (error) {
|
|
88
89
|
el.innerHTML = `
|
|
89
90
|
<div class="sb-scene-debug-error">
|
|
90
|
-
<div class="sb-scene-debug-error-title">Error loading
|
|
91
|
+
<div class="sb-scene-debug-error-title">Error loading flow</div>
|
|
91
92
|
<p class="sb-scene-debug-error-message">${error}</p>
|
|
92
93
|
</div>`
|
|
93
94
|
} else {
|
|
94
95
|
const title = document.createElement('h2')
|
|
95
96
|
title.className = 'sb-scene-debug-title'
|
|
96
|
-
title.textContent = `
|
|
97
|
+
title.textContent = `Flow: ${activeFlowName}`
|
|
97
98
|
|
|
98
99
|
const pre = document.createElement('pre')
|
|
99
100
|
pre.className = 'sb-scene-debug-code'
|
|
@@ -106,3 +107,6 @@ export function mountSceneDebug(container, sceneName) {
|
|
|
106
107
|
target.appendChild(el)
|
|
107
108
|
return el
|
|
108
109
|
}
|
|
110
|
+
|
|
111
|
+
/** @deprecated Use mountFlowDebug() */
|
|
112
|
+
export const mountSceneDebug = mountFlowDebug
|
package/src/sceneDebug.test.js
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
import { vi } from 'vitest'
|
|
2
2
|
|
|
3
|
-
const {
|
|
4
|
-
|
|
3
|
+
const { mockLoadFlow } = vi.hoisted(() => ({
|
|
4
|
+
mockLoadFlow: vi.fn(() => ({ hello: 'world', count: 42 })),
|
|
5
5
|
}))
|
|
6
6
|
|
|
7
7
|
vi.mock('./loader.js', () => ({
|
|
8
|
-
|
|
8
|
+
loadFlow: mockLoadFlow,
|
|
9
9
|
}))
|
|
10
10
|
|
|
11
11
|
// We need a fresh module for each test since sceneDebug has a module-level
|
|
12
12
|
// `stylesInjected` boolean. We test style injection on the very first call,
|
|
13
13
|
// then subsequent tests just verify other behavior.
|
|
14
|
-
import { mountSceneDebug } from './sceneDebug.js'
|
|
14
|
+
import { mountFlowDebug, mountSceneDebug } from './sceneDebug.js'
|
|
15
15
|
|
|
16
16
|
afterEach(() => {
|
|
17
17
|
document.body.innerHTML = ''
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
mockLoadFlow.mockReset()
|
|
19
|
+
mockLoadFlow.mockReturnValue({ hello: 'world', count: 42 })
|
|
20
20
|
})
|
|
21
21
|
|
|
22
|
-
describe('
|
|
22
|
+
describe('mountFlowDebug', () => {
|
|
23
23
|
it('injects styles into document.head on first call', () => {
|
|
24
24
|
// This MUST run first to capture the stylesInjected=false → true transition
|
|
25
|
-
|
|
25
|
+
mountFlowDebug()
|
|
26
26
|
|
|
27
27
|
const styles = document.head.querySelectorAll('style')
|
|
28
28
|
const hasDebugStyle = Array.from(styles).some((el) =>
|
|
@@ -32,13 +32,13 @@ describe('mountSceneDebug', () => {
|
|
|
32
32
|
})
|
|
33
33
|
|
|
34
34
|
it('creates an element with class sb-scene-debug', () => {
|
|
35
|
-
const el =
|
|
35
|
+
const el = mountFlowDebug()
|
|
36
36
|
|
|
37
37
|
expect(el.classList.contains('sb-scene-debug')).toBe(true)
|
|
38
38
|
})
|
|
39
39
|
|
|
40
40
|
it('appends to document.body by default', () => {
|
|
41
|
-
|
|
41
|
+
mountFlowDebug()
|
|
42
42
|
|
|
43
43
|
expect(document.body.querySelector('.sb-scene-debug')).toBeInTheDocument()
|
|
44
44
|
})
|
|
@@ -47,36 +47,36 @@ describe('mountSceneDebug', () => {
|
|
|
47
47
|
const container = document.createElement('div')
|
|
48
48
|
document.body.appendChild(container)
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
mountFlowDebug(container)
|
|
51
51
|
|
|
52
52
|
expect(container.querySelector('.sb-scene-debug')).not.toBeNull()
|
|
53
53
|
})
|
|
54
54
|
|
|
55
55
|
it('returns the created element', () => {
|
|
56
|
-
const el =
|
|
56
|
+
const el = mountFlowDebug()
|
|
57
57
|
|
|
58
58
|
expect(el).toBeInstanceOf(HTMLElement)
|
|
59
59
|
expect(el.className).toBe('sb-scene-debug')
|
|
60
60
|
})
|
|
61
61
|
|
|
62
|
-
it('renders the
|
|
63
|
-
|
|
62
|
+
it('renders the flow name in the title', () => {
|
|
63
|
+
mountFlowDebug(undefined, 'my-flow')
|
|
64
64
|
|
|
65
65
|
const title = document.body.querySelector('.sb-scene-debug-title')
|
|
66
66
|
expect(title).not.toBeNull()
|
|
67
|
-
expect(title.textContent).toContain('my-
|
|
67
|
+
expect(title.textContent).toContain('my-flow')
|
|
68
68
|
})
|
|
69
69
|
|
|
70
|
-
it('defaults
|
|
71
|
-
|
|
70
|
+
it('defaults flow name to "default" when none is provided', () => {
|
|
71
|
+
mountFlowDebug()
|
|
72
72
|
|
|
73
73
|
const title = document.body.querySelector('.sb-scene-debug-title')
|
|
74
74
|
expect(title.textContent).toContain('default')
|
|
75
|
-
expect(
|
|
75
|
+
expect(mockLoadFlow).toHaveBeenCalledWith('default')
|
|
76
76
|
})
|
|
77
77
|
|
|
78
78
|
it('renders JSON data in a pre element', () => {
|
|
79
|
-
|
|
79
|
+
mountFlowDebug()
|
|
80
80
|
|
|
81
81
|
const pre = document.body.querySelector('.sb-scene-debug-code')
|
|
82
82
|
expect(pre).not.toBeNull()
|
|
@@ -86,31 +86,31 @@ describe('mountSceneDebug', () => {
|
|
|
86
86
|
expect(parsed).toEqual({ hello: 'world', count: 42 })
|
|
87
87
|
})
|
|
88
88
|
|
|
89
|
-
it('shows error when
|
|
90
|
-
|
|
91
|
-
throw new Error('
|
|
89
|
+
it('shows error when loadFlow throws', () => {
|
|
90
|
+
mockLoadFlow.mockImplementation(() => {
|
|
91
|
+
throw new Error('Flow not found')
|
|
92
92
|
})
|
|
93
93
|
|
|
94
|
-
const el =
|
|
94
|
+
const el = mountFlowDebug()
|
|
95
95
|
|
|
96
96
|
const errorTitle = el.querySelector('.sb-scene-debug-error-title')
|
|
97
97
|
expect(errorTitle).not.toBeNull()
|
|
98
98
|
expect(errorTitle.textContent).toContain('Error')
|
|
99
99
|
|
|
100
100
|
const errorMsg = el.querySelector('.sb-scene-debug-error-message')
|
|
101
|
-
expect(errorMsg.textContent).toContain('
|
|
101
|
+
expect(errorMsg.textContent).toContain('Flow not found')
|
|
102
102
|
|
|
103
103
|
// Should NOT render the normal title/pre
|
|
104
104
|
expect(el.querySelector('.sb-scene-debug-title')).toBeNull()
|
|
105
105
|
expect(el.querySelector('.sb-scene-debug-code')).toBeNull()
|
|
106
106
|
})
|
|
107
107
|
|
|
108
|
-
it('uses ?scene query param when no
|
|
108
|
+
it('uses ?scene query param when no flowName argument is given', () => {
|
|
109
109
|
window.history.pushState(null, '', '?scene=overview')
|
|
110
110
|
|
|
111
|
-
|
|
111
|
+
mountFlowDebug()
|
|
112
112
|
|
|
113
|
-
expect(
|
|
113
|
+
expect(mockLoadFlow).toHaveBeenCalledWith('overview')
|
|
114
114
|
const title = document.body.querySelector('.sb-scene-debug-title')
|
|
115
115
|
expect(title.textContent).toContain('overview')
|
|
116
116
|
|
|
@@ -119,10 +119,23 @@ describe('mountSceneDebug', () => {
|
|
|
119
119
|
})
|
|
120
120
|
|
|
121
121
|
it('allows multiple debug panels to be mounted', () => {
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
mountFlowDebug()
|
|
123
|
+
mountFlowDebug()
|
|
124
124
|
|
|
125
125
|
const panels = document.body.querySelectorAll('.sb-scene-debug')
|
|
126
126
|
expect(panels).toHaveLength(2)
|
|
127
127
|
})
|
|
128
128
|
})
|
|
129
|
+
|
|
130
|
+
// ── mountSceneDebug (deprecated alias) ──
|
|
131
|
+
|
|
132
|
+
describe('mountSceneDebug (deprecated alias)', () => {
|
|
133
|
+
it('is the same function as mountFlowDebug', () => {
|
|
134
|
+
expect(mountSceneDebug).toBe(mountFlowDebug)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('mounts a debug panel', () => {
|
|
138
|
+
const el = mountSceneDebug()
|
|
139
|
+
expect(el.classList.contains('sb-scene-debug')).toBe(true)
|
|
140
|
+
})
|
|
141
|
+
})
|
|
@@ -2,7 +2,9 @@ import { describe, it, expect, afterEach } from 'vitest'
|
|
|
2
2
|
import { render, screen } from '@testing-library/svelte'
|
|
3
3
|
import {
|
|
4
4
|
registerMode,
|
|
5
|
-
|
|
5
|
+
initTools,
|
|
6
|
+
setToolAction,
|
|
7
|
+
setToolState,
|
|
6
8
|
} from '@dfosco/storyboard-core'
|
|
7
9
|
import { _resetModes } from '@test/modes'
|
|
8
10
|
import ToolbarShell from '../components/ToolbarShell.svelte'
|
|
@@ -15,20 +17,22 @@ afterEach(() => {
|
|
|
15
17
|
})
|
|
16
18
|
|
|
17
19
|
describe('ToolbarShell', () => {
|
|
18
|
-
it('renders nothing when
|
|
20
|
+
it('renders nothing when no tools are declared', () => {
|
|
19
21
|
registerMode('prototype', { label: 'Navigate' })
|
|
20
22
|
const { container } = render(ToolbarShell)
|
|
21
23
|
expect(container.querySelector('[role="toolbar"]')).toBeNull()
|
|
22
24
|
})
|
|
23
25
|
|
|
24
|
-
it('renders tool buttons
|
|
25
|
-
registerMode('prototype', {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
{ id: 'zoom', label: 'Zoom',
|
|
29
|
-
{ id: 'pan', label: 'Pan',
|
|
26
|
+
it('renders tool buttons from the tool registry', () => {
|
|
27
|
+
registerMode('prototype', { label: 'Navigate' })
|
|
28
|
+
initTools({
|
|
29
|
+
'*': [
|
|
30
|
+
{ id: 'zoom', label: 'Zoom', group: 'tools' },
|
|
31
|
+
{ id: 'pan', label: 'Pan', group: 'tools' },
|
|
30
32
|
],
|
|
31
33
|
})
|
|
34
|
+
setToolAction('zoom', () => {})
|
|
35
|
+
setToolAction('pan', () => {})
|
|
32
36
|
|
|
33
37
|
render(ToolbarShell)
|
|
34
38
|
|
|
@@ -36,13 +40,12 @@ describe('ToolbarShell', () => {
|
|
|
36
40
|
expect(screen.getByTitle('Pan')).toBeInTheDocument()
|
|
37
41
|
})
|
|
38
42
|
|
|
39
|
-
it('renders dev tools section
|
|
40
|
-
registerMode('prototype', {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
{ id: 'debug', label: 'Debug', action: () => {} },
|
|
44
|
-
],
|
|
43
|
+
it('renders dev tools section', () => {
|
|
44
|
+
registerMode('prototype', { label: 'Navigate' })
|
|
45
|
+
initTools({
|
|
46
|
+
'*': [{ id: 'debug', label: 'Debug', group: 'dev' }],
|
|
45
47
|
})
|
|
48
|
+
setToolAction('debug', () => {})
|
|
46
49
|
|
|
47
50
|
render(ToolbarShell)
|
|
48
51
|
|
|
@@ -50,12 +53,16 @@ describe('ToolbarShell', () => {
|
|
|
50
53
|
expect(screen.getByText('Dev')).toBeInTheDocument()
|
|
51
54
|
})
|
|
52
55
|
|
|
53
|
-
it('renders both tool groups
|
|
54
|
-
registerMode('prototype', {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
it('renders both tool groups', () => {
|
|
57
|
+
registerMode('prototype', { label: 'Navigate' })
|
|
58
|
+
initTools({
|
|
59
|
+
'*': [
|
|
60
|
+
{ id: 'zoom', label: 'Zoom', group: 'tools' },
|
|
61
|
+
{ id: 'debug', label: 'Debug', group: 'dev' },
|
|
62
|
+
],
|
|
58
63
|
})
|
|
64
|
+
setToolAction('zoom', () => {})
|
|
65
|
+
setToolAction('debug', () => {})
|
|
59
66
|
|
|
60
67
|
render(ToolbarShell)
|
|
61
68
|
|
|
@@ -64,4 +71,55 @@ describe('ToolbarShell', () => {
|
|
|
64
71
|
expect(screen.getByText('Tools')).toBeInTheDocument()
|
|
65
72
|
expect(screen.getByText('Dev')).toBeInTheDocument()
|
|
66
73
|
})
|
|
74
|
+
|
|
75
|
+
it('disables tools without an action', () => {
|
|
76
|
+
registerMode('prototype', { label: 'Navigate' })
|
|
77
|
+
initTools({
|
|
78
|
+
'*': [{ id: 'no-action', label: 'No Action', group: 'tools' }],
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
render(ToolbarShell)
|
|
82
|
+
|
|
83
|
+
const btn = screen.getByTitle('No Action')
|
|
84
|
+
expect(btn).toBeDisabled()
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('disables tools with enabled: false state', () => {
|
|
88
|
+
registerMode('prototype', { label: 'Navigate' })
|
|
89
|
+
initTools({
|
|
90
|
+
'*': [{ id: 'disabled-tool', label: 'Disabled', group: 'tools' }],
|
|
91
|
+
})
|
|
92
|
+
setToolAction('disabled-tool', () => {})
|
|
93
|
+
setToolState('disabled-tool', { enabled: false })
|
|
94
|
+
|
|
95
|
+
render(ToolbarShell)
|
|
96
|
+
|
|
97
|
+
const btn = screen.getByTitle('Disabled')
|
|
98
|
+
expect(btn).toBeDisabled()
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('hides tools with hidden: true state', () => {
|
|
102
|
+
registerMode('prototype', { label: 'Navigate' })
|
|
103
|
+
initTools({
|
|
104
|
+
'*': [{ id: 'hidden-tool', label: 'Hidden', group: 'tools' }],
|
|
105
|
+
})
|
|
106
|
+
setToolState('hidden-tool', { hidden: true })
|
|
107
|
+
|
|
108
|
+
const { container } = render(ToolbarShell)
|
|
109
|
+
|
|
110
|
+
expect(container.querySelector('[role="toolbar"]')).toBeNull()
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('renders badge when present', () => {
|
|
114
|
+
registerMode('prototype', { label: 'Navigate' })
|
|
115
|
+
initTools({
|
|
116
|
+
'*': [{ id: 'badged', label: 'Badged', group: 'tools' }],
|
|
117
|
+
})
|
|
118
|
+
setToolAction('badged', () => {})
|
|
119
|
+
setToolState('badged', { badge: 5 })
|
|
120
|
+
|
|
121
|
+
render(ToolbarShell)
|
|
122
|
+
|
|
123
|
+
expect(screen.getByText('5')).toBeInTheDocument()
|
|
124
|
+
})
|
|
67
125
|
})
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
registerMode,
|
|
4
4
|
} from '@dfosco/storyboard-core'
|
|
5
5
|
import { _resetModes } from '@test/modes'
|
|
6
|
-
import { mountDesignModesUI, unmountDesignModesUI } from '
|
|
6
|
+
import { mountDesignModesUI, unmountDesignModesUI } from '../../ui/design-modes.js'
|
|
7
7
|
|
|
8
8
|
afterEach(() => {
|
|
9
9
|
unmountDesignModesUI()
|