@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/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 SceneDebug — a vanilla JS debug panel that displays scene data.
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 { mountSceneDebug } from '@dfosco/storyboard-core'
8
- * mountSceneDebug(document.getElementById('debug'))
7
+ * import { mountFlowDebug } from '@dfosco/storyboard-core'
8
+ * mountFlowDebug(document.getElementById('debug'))
9
9
  * // or
10
- * mountSceneDebug() // appends to document.body
10
+ * mountFlowDebug() // appends to document.body
11
11
  */
12
- import { loadScene } from './loader.js'
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 scene debug panel into the DOM.
56
+ * Mount a flow debug panel into the DOM.
57
57
  *
58
58
  * @param {HTMLElement} [container=document.body] - Where to mount
59
- * @param {string} [sceneName] - Scene name override (defaults to ?scene= param or "default")
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 mountSceneDebug(container, sceneName) {
62
+ export function mountFlowDebug(container, flowName) {
63
63
  const target = container || document.body
64
- const activeSceneName = sceneName
65
- || new URLSearchParams(window.location.search).get('scene')
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 = loadScene(activeSceneName)
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 scene</div>
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 = `Scene: ${activeSceneName}`
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
@@ -1,28 +1,28 @@
1
1
  import { vi } from 'vitest'
2
2
 
3
- const { mockLoadScene } = vi.hoisted(() => ({
4
- mockLoadScene: vi.fn(() => ({ hello: 'world', count: 42 })),
3
+ const { mockLoadFlow } = vi.hoisted(() => ({
4
+ mockLoadFlow: vi.fn(() => ({ hello: 'world', count: 42 })),
5
5
  }))
6
6
 
7
7
  vi.mock('./loader.js', () => ({
8
- loadScene: mockLoadScene,
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
- mockLoadScene.mockReset()
19
- mockLoadScene.mockReturnValue({ hello: 'world', count: 42 })
18
+ mockLoadFlow.mockReset()
19
+ mockLoadFlow.mockReturnValue({ hello: 'world', count: 42 })
20
20
  })
21
21
 
22
- describe('mountSceneDebug', () => {
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
- mountSceneDebug()
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 = mountSceneDebug()
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
- mountSceneDebug()
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
- mountSceneDebug(container)
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 = mountSceneDebug()
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 scene name in the title', () => {
63
- mountSceneDebug(undefined, 'my-scene')
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-scene')
67
+ expect(title.textContent).toContain('my-flow')
68
68
  })
69
69
 
70
- it('defaults scene name to "default" when none is provided', () => {
71
- mountSceneDebug()
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(mockLoadScene).toHaveBeenCalledWith('default')
75
+ expect(mockLoadFlow).toHaveBeenCalledWith('default')
76
76
  })
77
77
 
78
78
  it('renders JSON data in a pre element', () => {
79
- mountSceneDebug()
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 loadScene throws', () => {
90
- mockLoadScene.mockImplementation(() => {
91
- throw new Error('Scene not found')
89
+ it('shows error when loadFlow throws', () => {
90
+ mockLoadFlow.mockImplementation(() => {
91
+ throw new Error('Flow not found')
92
92
  })
93
93
 
94
- const el = mountSceneDebug()
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('Scene not found')
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 sceneName argument is given', () => {
108
+ it('uses ?scene query param when no flowName argument is given', () => {
109
109
  window.history.pushState(null, '', '?scene=overview')
110
110
 
111
- mountSceneDebug()
111
+ mountFlowDebug()
112
112
 
113
- expect(mockLoadScene).toHaveBeenCalledWith('overview')
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
- mountSceneDebug()
123
- mountSceneDebug()
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
- activateMode,
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 current mode has no tools', () => {
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 for mode with tools', () => {
25
- registerMode('prototype', {
26
- label: 'Navigate',
27
- tools: [
28
- { id: 'zoom', label: 'Zoom', action: () => {} },
29
- { id: 'pan', label: 'Pan', action: () => {} },
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 when mode has devTools', () => {
40
- registerMode('prototype', {
41
- label: 'Navigate',
42
- devTools: [
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 when mode has both', () => {
54
- registerMode('prototype', {
55
- label: 'Navigate',
56
- tools: [{ id: 'zoom', label: 'Zoom', action: () => {} }],
57
- devTools: [{ id: 'debug', label: 'Debug', action: () => {} }],
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 '../plugins/design-modes.js'
6
+ import { mountDesignModesUI, unmountDesignModesUI } from '../../ui/design-modes.js'
7
7
 
8
8
  afterEach(() => {
9
9
  unmountDesignModesUI()