gitmaps 1.1.0 → 1.1.2

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.
Files changed (121) hide show
  1. package/README.md +267 -118
  2. package/app/[...slug]/page.client.tsx +1 -0
  3. package/app/[...slug]/page.tsx +6 -0
  4. package/app/analytics.db +0 -0
  5. package/app/api/analytics/route.ts +64 -0
  6. package/app/api/auth/positions/route.ts +95 -33
  7. package/app/api/build-info/route.ts +19 -0
  8. package/app/api/chat/route.ts +13 -2
  9. package/app/api/og-image/route.ts +14 -0
  10. package/app/api/repo/file-content/route.ts +73 -20
  11. package/app/api/repo/load/route.test.ts +62 -0
  12. package/app/api/repo/load/route.ts +41 -1
  13. package/app/api/repo/pdf-thumb/route.ts +127 -0
  14. package/app/api/repo/resolve-slug/route.ts +51 -0
  15. package/app/api/repo/tree/route.ts +188 -104
  16. package/app/api/version/route.ts +26 -0
  17. package/app/globals.css +5706 -4938
  18. package/app/layout.tsx +1279 -490
  19. package/app/lib/auto-arrange.test.ts +158 -0
  20. package/app/lib/auto-arrange.ts +147 -0
  21. package/app/lib/canvas-export.ts +358 -358
  22. package/app/lib/canvas.ts +625 -564
  23. package/app/lib/cards.tsx +1361 -916
  24. package/app/lib/chat.tsx +65 -9
  25. package/app/lib/code-editor.ts +86 -2
  26. package/app/lib/context.test.ts +32 -0
  27. package/app/lib/context.ts +19 -3
  28. package/app/lib/cursor-sharing.ts +34 -0
  29. package/app/lib/events.tsx +71 -93
  30. package/app/lib/export-canvas.ts +287 -0
  31. package/app/lib/file-card-plugin.ts +148 -148
  32. package/app/lib/file-modal.tsx +49 -0
  33. package/app/lib/file-preview.ts +486 -427
  34. package/app/lib/github-import.test.ts +424 -0
  35. package/app/lib/initial-route-hydration.test.ts +283 -0
  36. package/app/lib/initial-route-hydration.ts +202 -0
  37. package/app/lib/landing-reset.test.ts +99 -0
  38. package/app/lib/landing-reset.ts +106 -0
  39. package/app/lib/landing-shell.test.ts +75 -0
  40. package/app/lib/large-repo-optimization.ts +37 -0
  41. package/app/lib/layout-snapshots.ts +320 -0
  42. package/app/lib/loading.test.ts +69 -0
  43. package/app/lib/loading.tsx +160 -45
  44. package/app/lib/mount-cleanup.test.ts +52 -0
  45. package/app/lib/mount-cleanup.ts +34 -0
  46. package/app/lib/mount-init.test.ts +123 -0
  47. package/app/lib/mount-init.ts +107 -0
  48. package/app/lib/mount-lifecycle.test.ts +39 -0
  49. package/app/lib/mount-lifecycle.ts +12 -0
  50. package/app/lib/mount-route-wiring.test.ts +87 -0
  51. package/app/lib/mount-route-wiring.ts +84 -0
  52. package/app/lib/multi-repo.ts +14 -0
  53. package/app/lib/onboarding-tutorial.ts +278 -0
  54. package/app/lib/positions.ts +190 -121
  55. package/app/lib/recent-commits.test.ts +947 -0
  56. package/app/lib/recent-commits.ts +227 -0
  57. package/app/lib/repo-handoff.test.ts +23 -0
  58. package/app/lib/repo-handoff.ts +16 -0
  59. package/app/lib/repo-progressive.ts +119 -0
  60. package/app/lib/repo-select.test.ts +61 -0
  61. package/app/lib/repo-select.ts +74 -0
  62. package/app/lib/repo.tsx +1383 -987
  63. package/app/lib/role.ts +228 -0
  64. package/app/lib/route-catchall.test.ts +27 -0
  65. package/app/lib/route-repo-entry.test.ts +95 -0
  66. package/app/lib/route-repo-entry.ts +36 -0
  67. package/app/lib/router-contract.test.ts +22 -0
  68. package/app/lib/router-contract.ts +19 -0
  69. package/app/lib/shared-layout.test.ts +86 -0
  70. package/app/lib/shared-layout.ts +82 -0
  71. package/app/lib/status-bar.test.ts +118 -0
  72. package/app/lib/status-bar.ts +365 -128
  73. package/app/lib/sync-controls.test.ts +43 -0
  74. package/app/lib/sync-controls.tsx +303 -0
  75. package/app/lib/test-dom.ts +145 -0
  76. package/app/lib/test-fixtures/router-contract/[...slug]/page.tsx +3 -0
  77. package/app/lib/test-fixtures/router-contract/api/health/route.ts +3 -0
  78. package/app/lib/test-fixtures/router-contract/api/version/route.ts +3 -0
  79. package/app/lib/test-fixtures/router-contract/galaxy-canvas/page.tsx +3 -0
  80. package/app/lib/test-fixtures/router-contract/page.tsx +3 -0
  81. package/app/lib/transclusion-smoke.test.ts +163 -0
  82. package/app/lib/tutorial.ts +301 -0
  83. package/app/lib/version.ts +93 -0
  84. package/app/lib/viewport-culling.ts +740 -735
  85. package/app/lib/virtual-files.ts +456 -0
  86. package/app/lib/webgl-text.ts +189 -0
  87. package/app/lib/{galaxydraw-bridge.ts → xydraw-bridge.ts} +485 -482
  88. package/app/lib/{galaxydraw.test.ts → xydraw.test.ts} +228 -229
  89. package/app/og-image.png +0 -0
  90. package/app/page.client.tsx +70 -269
  91. package/app/page.tsx +15 -16
  92. package/app/state/machine.js +13 -0
  93. package/package.json +84 -75
  94. package/server.ts +10 -0
  95. package/app/[owner]/[repo]/page.tsx +0 -6
  96. package/app/[slug]/page.tsx +0 -6
  97. package/packages/galaxydraw/README.md +0 -296
  98. package/packages/galaxydraw/banner.png +0 -0
  99. package/packages/galaxydraw/demo/build-static.ts +0 -100
  100. package/packages/galaxydraw/demo/client.ts +0 -154
  101. package/packages/galaxydraw/demo/dist/client.js +0 -8
  102. package/packages/galaxydraw/demo/index.html +0 -256
  103. package/packages/galaxydraw/demo/server.ts +0 -96
  104. package/packages/galaxydraw/dist/index.js +0 -984
  105. package/packages/galaxydraw/dist/index.js.map +0 -16
  106. package/packages/galaxydraw/node_modules/.bin/tsc.bunx +0 -0
  107. package/packages/galaxydraw/node_modules/.bin/tsc.exe +0 -0
  108. package/packages/galaxydraw/node_modules/.bin/tsserver.bunx +0 -0
  109. package/packages/galaxydraw/node_modules/.bin/tsserver.exe +0 -0
  110. package/packages/galaxydraw/package.json +0 -49
  111. package/packages/galaxydraw/perf.test.ts +0 -284
  112. package/packages/galaxydraw/src/core/cards.ts +0 -435
  113. package/packages/galaxydraw/src/core/engine.ts +0 -339
  114. package/packages/galaxydraw/src/core/events.ts +0 -81
  115. package/packages/galaxydraw/src/core/layout.ts +0 -136
  116. package/packages/galaxydraw/src/core/minimap.ts +0 -216
  117. package/packages/galaxydraw/src/core/state.ts +0 -177
  118. package/packages/galaxydraw/src/core/viewport.ts +0 -106
  119. package/packages/galaxydraw/src/galaxydraw.css +0 -166
  120. package/packages/galaxydraw/src/index.ts +0 -40
  121. package/packages/galaxydraw/tsconfig.json +0 -30
@@ -1,229 +1,228 @@
1
- /**
2
- * galaxydraw core unit tests — CanvasState & EventBus
3
- *
4
- * Pure logic tests (no DOM). Validates coordinate conversion,
5
- * zoom clamping, snapshot/subscribe, and pub/sub.
6
- *
7
- * Run: bun test app/lib/galaxydraw.test.ts
8
- */
9
- import { describe, expect, test } from 'bun:test'
10
- import { CanvasState } from 'galaxydraw'
11
- import { EventBus } from 'galaxydraw'
12
-
13
- // ─── CanvasState ────────────────────────────────────────
14
-
15
- describe('CanvasState', () => {
16
- test('initial state is zoom=1, offset=0,0', () => {
17
- const s = new CanvasState()
18
- expect(s.zoom).toBe(1)
19
- expect(s.offsetX).toBe(0)
20
- expect(s.offsetY).toBe(0)
21
- })
22
-
23
- test('snapshot returns a copy', () => {
24
- const s = new CanvasState()
25
- const snap = s.snapshot()
26
- expect(snap).toEqual({ zoom: 1, offsetX: 0, offsetY: 0 })
27
-
28
- // Mutation doesn't affect original
29
- snap.zoom = 999
30
- expect(s.zoom).toBe(1)
31
- })
32
-
33
- test('set() updates zoom and offset', () => {
34
- const s = new CanvasState()
35
- s.set(2, 100, 200)
36
- expect(s.zoom).toBe(2)
37
- expect(s.offsetX).toBe(100)
38
- expect(s.offsetY).toBe(200)
39
- })
40
-
41
- test('set() clamps zoom to MIN_ZOOM', () => {
42
- const s = new CanvasState()
43
- s.set(0.001, 0, 0)
44
- expect(s.zoom).toBe(s.MIN_ZOOM)
45
- })
46
-
47
- test('set() clamps zoom to MAX_ZOOM', () => {
48
- const s = new CanvasState()
49
- s.set(100, 0, 0)
50
- expect(s.zoom).toBe(s.MAX_ZOOM)
51
- })
52
-
53
- test('pan() accumulates delta', () => {
54
- const s = new CanvasState()
55
- s.pan(10, 20)
56
- expect(s.offsetX).toBe(10)
57
- expect(s.offsetY).toBe(20)
58
- s.pan(5, -10)
59
- expect(s.offsetX).toBe(15)
60
- expect(s.offsetY).toBe(10)
61
- })
62
-
63
- test('subscribe() is called on set()', () => {
64
- const s = new CanvasState()
65
- let callCount = 0
66
- s.subscribe(() => { callCount++ })
67
- s.set(2, 0, 0)
68
- expect(callCount).toBe(1)
69
- })
70
-
71
- test('unsubscribe works', () => {
72
- const s = new CanvasState()
73
- let callCount = 0
74
- const unsub = s.subscribe(() => { callCount++ })
75
- s.set(2, 0, 0)
76
- expect(callCount).toBe(1)
77
- unsub()
78
- s.set(3, 0, 0)
79
- expect(callCount).toBe(1) // No additional call
80
- })
81
-
82
- test('subscribe() is called on pan()', () => {
83
- const s = new CanvasState()
84
- let called = false
85
- s.subscribe(() => { called = true })
86
- s.pan(10, 20)
87
- expect(called).toBe(true)
88
- })
89
-
90
- test('screenToWorld identity at zoom=1 offset=0 (no viewport)', () => {
91
- const s = new CanvasState()
92
- // Without a viewport, rect defaults are 0, so screenToWorld
93
- // just divides by zoom and subtracts offset
94
- const p = s.screenToWorld(100, 200)
95
- expect(p.x).toBe(100)
96
- expect(p.y).toBe(200)
97
- })
98
-
99
- test('screenToWorld with zoom=2', () => {
100
- const s = new CanvasState()
101
- s.set(2, 0, 0)
102
- const p = s.screenToWorld(200, 400)
103
- expect(p.x).toBe(100)
104
- expect(p.y).toBe(200)
105
- })
106
-
107
- test('screenToWorld with offset', () => {
108
- const s = new CanvasState()
109
- s.set(1, 50, 100)
110
- const p = s.screenToWorld(150, 200)
111
- expect(p.x).toBe(100)
112
- expect(p.y).toBe(100)
113
- })
114
-
115
- test('worldToScreen identity at zoom=1 offset=0 (no viewport)', () => {
116
- const s = new CanvasState()
117
- const p = s.worldToScreen(100, 200)
118
- expect(p.x).toBe(100)
119
- expect(p.y).toBe(200)
120
- })
121
-
122
- test('worldToScreen with zoom=2', () => {
123
- const s = new CanvasState()
124
- s.set(2, 0, 0)
125
- const p = s.worldToScreen(100, 200)
126
- expect(p.x).toBe(200)
127
- expect(p.y).toBe(400)
128
- })
129
-
130
- test('screenToWorld/worldToScreen roundtrip', () => {
131
- const s = new CanvasState()
132
- s.set(1.5, 30, -40)
133
- const world = s.screenToWorld(300, 250)
134
- const screen = s.worldToScreen(world.x, world.y)
135
- expect(screen.x).toBeCloseTo(300, 5)
136
- expect(screen.y).toBeCloseTo(250, 5)
137
- })
138
- })
139
-
140
- // ─── EventBus ───────────────────────────────────────────
141
-
142
- describe('EventBus', () => {
143
- test('on() receives emitted events', () => {
144
- const bus = new EventBus()
145
- let received: any = null
146
- bus.on('canvas:pan', (data) => { received = data })
147
- bus.emit('canvas:pan', { offsetX: 10, offsetY: 20 })
148
- expect(received).toEqual({ offsetX: 10, offsetY: 20 })
149
- })
150
-
151
- test('multiple handlers receive same event', () => {
152
- const bus = new EventBus()
153
- let count = 0
154
- bus.on('canvas:zoom', () => { count++ })
155
- bus.on('canvas:zoom', () => { count++ })
156
- bus.emit('canvas:zoom', { zoom: 2, centerX: 0, centerY: 0 })
157
- expect(count).toBe(2)
158
- })
159
-
160
- test('unsubscribe via returned function', () => {
161
- const bus = new EventBus()
162
- let count = 0
163
- const unsub = bus.on('canvas:pan', () => { count++ })
164
- bus.emit('canvas:pan', { offsetX: 0, offsetY: 0 })
165
- expect(count).toBe(1)
166
- unsub()
167
- bus.emit('canvas:pan', { offsetX: 0, offsetY: 0 })
168
- expect(count).toBe(1)
169
- })
170
-
171
- test('once() fires only once', () => {
172
- const bus = new EventBus()
173
- let count = 0
174
- bus.once('card:create', () => { count++ })
175
- bus.emit('card:create', { id: '1', x: 0, y: 0 })
176
- bus.emit('card:create', { id: '2', x: 0, y: 0 })
177
- expect(count).toBe(1)
178
- })
179
-
180
- test('off() without handler removes all handlers for event', () => {
181
- const bus = new EventBus()
182
- let count = 0
183
- bus.on('canvas:pan', () => { count++ })
184
- bus.on('canvas:pan', () => { count++ })
185
- bus.off('canvas:pan')
186
- bus.emit('canvas:pan', { offsetX: 0, offsetY: 0 })
187
- expect(count).toBe(0)
188
- })
189
-
190
- test('off() with handler removes only that handler', () => {
191
- const bus = new EventBus()
192
- let aCount = 0
193
- let bCount = 0
194
- const handlerA = () => { aCount++ }
195
- const handlerB = () => { bCount++ }
196
- bus.on('canvas:pan', handlerA)
197
- bus.on('canvas:pan', handlerB)
198
- bus.off('canvas:pan', handlerA)
199
- bus.emit('canvas:pan', { offsetX: 0, offsetY: 0 })
200
- expect(aCount).toBe(0)
201
- expect(bCount).toBe(1)
202
- })
203
-
204
- test('clear() removes all event handlers', () => {
205
- const bus = new EventBus()
206
- let count = 0
207
- bus.on('canvas:pan', () => { count++ })
208
- bus.on('canvas:zoom', () => { count++ })
209
- bus.clear()
210
- bus.emit('canvas:pan', { offsetX: 0, offsetY: 0 })
211
- bus.emit('canvas:zoom', { zoom: 1, centerX: 0, centerY: 0 })
212
- expect(count).toBe(0)
213
- })
214
-
215
- test('emit with no handlers does not throw', () => {
216
- const bus = new EventBus()
217
- expect(() => bus.emit('canvas:pan', { offsetX: 0, offsetY: 0 })).not.toThrow()
218
- })
219
-
220
- test('handler error does not break other handlers', () => {
221
- const bus = new EventBus()
222
- let secondCalled = false
223
- bus.on('canvas:pan', () => { throw new Error('boom') })
224
- bus.on('canvas:pan', () => { secondCalled = true })
225
- // Should not throw, errors are caught internally
226
- bus.emit('canvas:pan', { offsetX: 0, offsetY: 0 })
227
- expect(secondCalled).toBe(true)
228
- })
229
- })
1
+ /**
2
+ * xydraw core unit tests — CanvasState & EventBus
3
+ *
4
+ * Pure logic tests (no DOM). Validates coordinate conversion,
5
+ * zoom clamping, snapshot/subscribe, and pub/sub.
6
+ *
7
+ * Run: bun test app/lib/xydraw.test.ts
8
+ */
9
+ import { describe, expect, test } from 'bun:test'
10
+ import { CanvasState, EventBus } from '../../packages/galaxydraw/src/index'
11
+
12
+ // ─── CanvasState ────────────────────────────────────────
13
+
14
+ describe('CanvasState', () => {
15
+ test('initial state is zoom=1, offset=0,0', () => {
16
+ const s = new CanvasState()
17
+ expect(s.zoom).toBe(1)
18
+ expect(s.offsetX).toBe(0)
19
+ expect(s.offsetY).toBe(0)
20
+ })
21
+
22
+ test('snapshot returns a copy', () => {
23
+ const s = new CanvasState()
24
+ const snap = s.snapshot()
25
+ expect(snap).toEqual({ zoom: 1, offsetX: 0, offsetY: 0 })
26
+
27
+ // Mutation doesn't affect original
28
+ snap.zoom = 999
29
+ expect(s.zoom).toBe(1)
30
+ })
31
+
32
+ test('set() updates zoom and offset', () => {
33
+ const s = new CanvasState()
34
+ s.set(2, 100, 200)
35
+ expect(s.zoom).toBe(2)
36
+ expect(s.offsetX).toBe(100)
37
+ expect(s.offsetY).toBe(200)
38
+ })
39
+
40
+ test('set() clamps zoom to MIN_ZOOM', () => {
41
+ const s = new CanvasState()
42
+ s.set(0.001, 0, 0)
43
+ expect(s.zoom).toBe(s.MIN_ZOOM)
44
+ })
45
+
46
+ test('set() clamps zoom to MAX_ZOOM', () => {
47
+ const s = new CanvasState()
48
+ s.set(100, 0, 0)
49
+ expect(s.zoom).toBe(s.MAX_ZOOM)
50
+ })
51
+
52
+ test('pan() accumulates delta', () => {
53
+ const s = new CanvasState()
54
+ s.pan(10, 20)
55
+ expect(s.offsetX).toBe(10)
56
+ expect(s.offsetY).toBe(20)
57
+ s.pan(5, -10)
58
+ expect(s.offsetX).toBe(15)
59
+ expect(s.offsetY).toBe(10)
60
+ })
61
+
62
+ test('subscribe() is called on set()', () => {
63
+ const s = new CanvasState()
64
+ let callCount = 0
65
+ s.subscribe(() => { callCount++ })
66
+ s.set(2, 0, 0)
67
+ expect(callCount).toBe(1)
68
+ })
69
+
70
+ test('unsubscribe works', () => {
71
+ const s = new CanvasState()
72
+ let callCount = 0
73
+ const unsub = s.subscribe(() => { callCount++ })
74
+ s.set(2, 0, 0)
75
+ expect(callCount).toBe(1)
76
+ unsub()
77
+ s.set(3, 0, 0)
78
+ expect(callCount).toBe(1) // No additional call
79
+ })
80
+
81
+ test('subscribe() is called on pan()', () => {
82
+ const s = new CanvasState()
83
+ let called = false
84
+ s.subscribe(() => { called = true })
85
+ s.pan(10, 20)
86
+ expect(called).toBe(true)
87
+ })
88
+
89
+ test('screenToWorld identity at zoom=1 offset=0 (no viewport)', () => {
90
+ const s = new CanvasState()
91
+ // Without a viewport, rect defaults are 0, so screenToWorld
92
+ // just divides by zoom and subtracts offset
93
+ const p = s.screenToWorld(100, 200)
94
+ expect(p.x).toBe(100)
95
+ expect(p.y).toBe(200)
96
+ })
97
+
98
+ test('screenToWorld with zoom=2', () => {
99
+ const s = new CanvasState()
100
+ s.set(2, 0, 0)
101
+ const p = s.screenToWorld(200, 400)
102
+ expect(p.x).toBe(100)
103
+ expect(p.y).toBe(200)
104
+ })
105
+
106
+ test('screenToWorld with offset', () => {
107
+ const s = new CanvasState()
108
+ s.set(1, 50, 100)
109
+ const p = s.screenToWorld(150, 200)
110
+ expect(p.x).toBe(100)
111
+ expect(p.y).toBe(100)
112
+ })
113
+
114
+ test('worldToScreen identity at zoom=1 offset=0 (no viewport)', () => {
115
+ const s = new CanvasState()
116
+ const p = s.worldToScreen(100, 200)
117
+ expect(p.x).toBe(100)
118
+ expect(p.y).toBe(200)
119
+ })
120
+
121
+ test('worldToScreen with zoom=2', () => {
122
+ const s = new CanvasState()
123
+ s.set(2, 0, 0)
124
+ const p = s.worldToScreen(100, 200)
125
+ expect(p.x).toBe(200)
126
+ expect(p.y).toBe(400)
127
+ })
128
+
129
+ test('screenToWorld/worldToScreen roundtrip', () => {
130
+ const s = new CanvasState()
131
+ s.set(1.5, 30, -40)
132
+ const world = s.screenToWorld(300, 250)
133
+ const screen = s.worldToScreen(world.x, world.y)
134
+ expect(screen.x).toBeCloseTo(300, 5)
135
+ expect(screen.y).toBeCloseTo(250, 5)
136
+ })
137
+ })
138
+
139
+ // ─── EventBus ───────────────────────────────────────────
140
+
141
+ describe('EventBus', () => {
142
+ test('on() receives emitted events', () => {
143
+ const bus = new EventBus()
144
+ let received: any = null
145
+ bus.on('canvas:pan', (data) => { received = data })
146
+ bus.emit('canvas:pan', { offsetX: 10, offsetY: 20 })
147
+ expect(received).toEqual({ offsetX: 10, offsetY: 20 })
148
+ })
149
+
150
+ test('multiple handlers receive same event', () => {
151
+ const bus = new EventBus()
152
+ let count = 0
153
+ bus.on('canvas:zoom', () => { count++ })
154
+ bus.on('canvas:zoom', () => { count++ })
155
+ bus.emit('canvas:zoom', { zoom: 2, centerX: 0, centerY: 0 })
156
+ expect(count).toBe(2)
157
+ })
158
+
159
+ test('unsubscribe via returned function', () => {
160
+ const bus = new EventBus()
161
+ let count = 0
162
+ const unsub = bus.on('canvas:pan', () => { count++ })
163
+ bus.emit('canvas:pan', { offsetX: 0, offsetY: 0 })
164
+ expect(count).toBe(1)
165
+ unsub()
166
+ bus.emit('canvas:pan', { offsetX: 0, offsetY: 0 })
167
+ expect(count).toBe(1)
168
+ })
169
+
170
+ test('once() fires only once', () => {
171
+ const bus = new EventBus()
172
+ let count = 0
173
+ bus.once('card:create', () => { count++ })
174
+ bus.emit('card:create', { id: '1', x: 0, y: 0 })
175
+ bus.emit('card:create', { id: '2', x: 0, y: 0 })
176
+ expect(count).toBe(1)
177
+ })
178
+
179
+ test('off() without handler removes all handlers for event', () => {
180
+ const bus = new EventBus()
181
+ let count = 0
182
+ bus.on('canvas:pan', () => { count++ })
183
+ bus.on('canvas:pan', () => { count++ })
184
+ bus.off('canvas:pan')
185
+ bus.emit('canvas:pan', { offsetX: 0, offsetY: 0 })
186
+ expect(count).toBe(0)
187
+ })
188
+
189
+ test('off() with handler removes only that handler', () => {
190
+ const bus = new EventBus()
191
+ let aCount = 0
192
+ let bCount = 0
193
+ const handlerA = () => { aCount++ }
194
+ const handlerB = () => { bCount++ }
195
+ bus.on('canvas:pan', handlerA)
196
+ bus.on('canvas:pan', handlerB)
197
+ bus.off('canvas:pan', handlerA)
198
+ bus.emit('canvas:pan', { offsetX: 0, offsetY: 0 })
199
+ expect(aCount).toBe(0)
200
+ expect(bCount).toBe(1)
201
+ })
202
+
203
+ test('clear() removes all event handlers', () => {
204
+ const bus = new EventBus()
205
+ let count = 0
206
+ bus.on('canvas:pan', () => { count++ })
207
+ bus.on('canvas:zoom', () => { count++ })
208
+ bus.clear()
209
+ bus.emit('canvas:pan', { offsetX: 0, offsetY: 0 })
210
+ bus.emit('canvas:zoom', { zoom: 1, centerX: 0, centerY: 0 })
211
+ expect(count).toBe(0)
212
+ })
213
+
214
+ test('emit with no handlers does not throw', () => {
215
+ const bus = new EventBus()
216
+ expect(() => bus.emit('canvas:pan', { offsetX: 0, offsetY: 0 })).not.toThrow()
217
+ })
218
+
219
+ test('handler error does not break other handlers', () => {
220
+ const bus = new EventBus()
221
+ let secondCalled = false
222
+ bus.on('canvas:pan', () => { throw new Error('boom') })
223
+ bus.on('canvas:pan', () => { secondCalled = true })
224
+ // Should not throw, errors are caught internally
225
+ bus.emit('canvas:pan', { offsetX: 0, offsetY: 0 })
226
+ expect(secondCalled).toBe(true)
227
+ })
228
+ })
Binary file