gitmaps 1.0.0 → 1.1.1

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 (145) hide show
  1. package/README.md +265 -122
  2. package/app/[...slug]/page.client.tsx +1 -0
  3. package/app/[...slug]/page.tsx +6 -0
  4. package/app/[owner]/[repo]/page.client.tsx +5 -0
  5. package/app/[slug]/page.client.tsx +5 -0
  6. package/app/analytics.db +0 -0
  7. package/app/api/analytics/route.ts +64 -0
  8. package/app/api/auth/positions/route.ts +95 -33
  9. package/app/api/build-info/route.ts +19 -0
  10. package/app/api/chat/route.ts +13 -2
  11. package/app/api/manifest.json/route.ts +20 -0
  12. package/app/api/og-image/route.ts +14 -0
  13. package/app/api/pwa-icon/route.ts +14 -0
  14. package/app/api/repo/clone-stream/route.ts +20 -12
  15. package/app/api/repo/file-content/route.ts +73 -20
  16. package/app/api/repo/imports/route.ts +21 -3
  17. package/app/api/repo/list/route.ts +30 -0
  18. package/app/api/repo/load/route.test.ts +62 -0
  19. package/app/api/repo/load/route.ts +41 -1
  20. package/app/api/repo/pdf-thumb/route.ts +127 -0
  21. package/app/api/repo/resolve-slug/route.ts +51 -0
  22. package/app/api/repo/tree/route.ts +188 -104
  23. package/app/api/repo/upload/route.ts +6 -9
  24. package/app/api/sw.js/route.ts +70 -0
  25. package/app/api/version/route.ts +26 -0
  26. package/app/galaxy-canvas/page.client.tsx +2 -0
  27. package/app/galaxy-canvas/page.tsx +5 -0
  28. package/app/globals.css +5844 -4694
  29. package/app/icon.png +0 -0
  30. package/app/layout.tsx +1284 -467
  31. package/app/lib/auto-arrange.test.ts +158 -0
  32. package/app/lib/auto-arrange.ts +147 -0
  33. package/app/lib/canvas-export.ts +358 -358
  34. package/app/lib/canvas-text.ts +4 -72
  35. package/app/lib/canvas.ts +625 -564
  36. package/app/lib/card-arrangement.ts +21 -7
  37. package/app/lib/card-context-menu.tsx +2 -2
  38. package/app/lib/card-groups.ts +9 -2
  39. package/app/lib/cards.tsx +1361 -914
  40. package/app/lib/chat.tsx +65 -9
  41. package/app/lib/code-editor.ts +86 -2
  42. package/app/lib/connections.tsx +34 -43
  43. package/app/lib/context.test.ts +32 -0
  44. package/app/lib/context.ts +19 -3
  45. package/app/lib/cursor-sharing.ts +34 -0
  46. package/app/lib/events.tsx +76 -73
  47. package/app/lib/export-canvas.ts +287 -0
  48. package/app/lib/file-card-plugin.ts +148 -134
  49. package/app/lib/file-modal.tsx +49 -0
  50. package/app/lib/file-preview.ts +486 -400
  51. package/app/lib/github-import.test.ts +424 -0
  52. package/app/lib/global-search.ts +48 -27
  53. package/app/lib/initial-route-hydration.test.ts +283 -0
  54. package/app/lib/initial-route-hydration.ts +202 -0
  55. package/app/lib/landing-reset.test.ts +99 -0
  56. package/app/lib/landing-reset.ts +106 -0
  57. package/app/lib/landing-shell.test.ts +75 -0
  58. package/app/lib/large-repo-optimization.ts +37 -0
  59. package/app/lib/layers.tsx +17 -18
  60. package/app/lib/layout-snapshots.ts +320 -0
  61. package/app/lib/loading.test.ts +69 -0
  62. package/app/lib/loading.tsx +160 -45
  63. package/app/lib/mount-cleanup.test.ts +52 -0
  64. package/app/lib/mount-cleanup.ts +34 -0
  65. package/app/lib/mount-init.test.ts +123 -0
  66. package/app/lib/mount-init.ts +107 -0
  67. package/app/lib/mount-lifecycle.test.ts +39 -0
  68. package/app/lib/mount-lifecycle.ts +12 -0
  69. package/app/lib/mount-route-wiring.test.ts +87 -0
  70. package/app/lib/mount-route-wiring.ts +84 -0
  71. package/app/lib/multi-repo.ts +14 -0
  72. package/app/lib/onboarding-tutorial.ts +278 -0
  73. package/app/lib/perf-overlay.ts +78 -0
  74. package/app/lib/positions.ts +191 -122
  75. package/app/lib/recent-commits.test.ts +869 -0
  76. package/app/lib/recent-commits.ts +227 -0
  77. package/app/lib/repo-handoff.test.ts +23 -0
  78. package/app/lib/repo-handoff.ts +16 -0
  79. package/app/lib/repo-progressive.ts +119 -0
  80. package/app/lib/repo-select.test.ts +61 -0
  81. package/app/lib/repo-select.ts +74 -0
  82. package/app/lib/repo.tsx +1383 -977
  83. package/app/lib/role.ts +228 -0
  84. package/app/lib/route-catchall.test.ts +27 -0
  85. package/app/lib/route-repo-entry.test.ts +95 -0
  86. package/app/lib/route-repo-entry.ts +36 -0
  87. package/app/lib/router-contract.test.ts +22 -0
  88. package/app/lib/router-contract.ts +19 -0
  89. package/app/lib/shared-layout.test.ts +86 -0
  90. package/app/lib/shared-layout.ts +82 -0
  91. package/app/lib/shortcuts-panel.ts +2 -0
  92. package/app/lib/status-bar.test.ts +118 -0
  93. package/app/lib/status-bar.ts +365 -128
  94. package/app/lib/sync-controls.test.ts +43 -0
  95. package/app/lib/sync-controls.tsx +303 -0
  96. package/app/lib/test-dom.ts +145 -0
  97. package/app/lib/test-fixtures/router-contract/[...slug]/page.tsx +3 -0
  98. package/app/lib/test-fixtures/router-contract/api/health/route.ts +3 -0
  99. package/app/lib/test-fixtures/router-contract/api/version/route.ts +3 -0
  100. package/app/lib/test-fixtures/router-contract/galaxy-canvas/page.tsx +3 -0
  101. package/app/lib/test-fixtures/router-contract/page.tsx +3 -0
  102. package/app/lib/transclusion-smoke.test.ts +163 -0
  103. package/app/lib/tutorial.ts +301 -0
  104. package/app/lib/version.ts +93 -0
  105. package/app/lib/viewport-culling.ts +740 -728
  106. package/app/lib/virtual-files.ts +456 -0
  107. package/app/lib/webgl-text.ts +189 -0
  108. package/app/lib/{galaxydraw-bridge.ts → xydraw-bridge.ts} +485 -477
  109. package/app/lib/{galaxydraw.test.ts → xydraw.test.ts} +228 -229
  110. package/app/og-image.png +0 -0
  111. package/app/page.client.tsx +70 -215
  112. package/app/page.tsx +27 -92
  113. package/app/state/machine.js +13 -0
  114. package/banner.png +0 -0
  115. package/package.json +17 -8
  116. package/server.ts +11 -1
  117. package/app/api/connections/route.ts +0 -72
  118. package/app/api/positions/route.ts +0 -80
  119. package/app/api/repo/browse/route.ts +0 -55
  120. package/app/lib/pr-review.ts +0 -374
  121. package/packages/galaxydraw/README.md +0 -296
  122. package/packages/galaxydraw/banner.png +0 -0
  123. package/packages/galaxydraw/demo/build-static.ts +0 -100
  124. package/packages/galaxydraw/demo/client.ts +0 -154
  125. package/packages/galaxydraw/demo/dist/client.js +0 -8
  126. package/packages/galaxydraw/demo/index.html +0 -256
  127. package/packages/galaxydraw/demo/server.ts +0 -96
  128. package/packages/galaxydraw/dist/index.js +0 -984
  129. package/packages/galaxydraw/dist/index.js.map +0 -16
  130. package/packages/galaxydraw/node_modules/.bin/tsc.bunx +0 -0
  131. package/packages/galaxydraw/node_modules/.bin/tsc.exe +0 -0
  132. package/packages/galaxydraw/node_modules/.bin/tsserver.bunx +0 -0
  133. package/packages/galaxydraw/node_modules/.bin/tsserver.exe +0 -0
  134. package/packages/galaxydraw/package.json +0 -49
  135. package/packages/galaxydraw/perf.test.ts +0 -284
  136. package/packages/galaxydraw/src/core/cards.ts +0 -435
  137. package/packages/galaxydraw/src/core/engine.ts +0 -339
  138. package/packages/galaxydraw/src/core/events.ts +0 -81
  139. package/packages/galaxydraw/src/core/layout.ts +0 -136
  140. package/packages/galaxydraw/src/core/minimap.ts +0 -216
  141. package/packages/galaxydraw/src/core/state.ts +0 -177
  142. package/packages/galaxydraw/src/core/viewport.ts +0 -106
  143. package/packages/galaxydraw/src/galaxydraw.css +0 -166
  144. package/packages/galaxydraw/src/index.ts +0 -40
  145. 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