@avimate/msfs-jest-utils 1.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,696 @@
1
+ "use strict";
2
+ /**
3
+ * SDK Adapter for Jest testing
4
+ *
5
+ * Provides mock implementations of MSFS SDK classes for testing.
6
+ * This allows components to be tested without the full SDK bundle.
7
+ *
8
+ * NOTE: We don't re-export types from @microsoft/msfs-types because
9
+ * they are declaration files and not modules. Types are resolved
10
+ * through TypeScript's type resolution.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.EventBus = exports.SubscribableUtils = exports.MapSystemBuilder = exports.MapWxrModule = exports.MapOwnAirplanePropsModule = exports.MapOwnAirplaneIconModule = exports.MapIndexedRangeModule = exports.BingComponent = exports.MapProjection = exports.FacilityLoader = exports.FacilityRepository = exports.MapSystemKeys = exports.Vec2Subject = exports.Vec2Math = exports.NumberUnitSubject = exports.UnitType = exports.UnitTypeClass = exports.NumberUnit = exports.Subject = exports.FSComponent = exports.DisplayComponent = void 0;
14
+ // Mock DisplayComponent
15
+ class DisplayComponent {
16
+ constructor(props) {
17
+ this.props = props;
18
+ }
19
+ onAfterRender(vnode) {
20
+ // Default implementation - can be overridden
21
+ }
22
+ destroy() {
23
+ // Default implementation - can be overridden
24
+ }
25
+ }
26
+ exports.DisplayComponent = DisplayComponent;
27
+ // Note: MSFSGlobals sets up FSComponent before this module is imported
28
+ const globalFSComponent = globalThis.FSComponent;
29
+ // Export the global FSComponent if available, otherwise use fallback
30
+ // (This should not happen if setupTests.ts runs correctly)
31
+ exports.FSComponent = globalFSComponent || {
32
+ buildComponent: (type, props, ...children) => {
33
+ // If it's a string (HTML/SVG tag), create a VNode structure
34
+ if (typeof type === 'string') {
35
+ const doc = globalThis.document;
36
+ if (!doc) {
37
+ return { type, props, children };
38
+ }
39
+ // Create actual DOM element
40
+ let element;
41
+ if (type === 'svg' || ['g', 'circle', 'text', 'line', 'polygon', 'path', 'rect', 'ellipse', 'polyline', 'defs', 'use', 'clipPath', 'mask', 'pattern', 'linearGradient', 'radialGradient', 'stop', 'filter', 'feGaussianBlur', 'feColorMatrix', 'feOffset', 'feMerge', 'feMergeNode'].includes(type)) {
42
+ element = doc.createElementNS('http://www.w3.org/2000/svg', type);
43
+ }
44
+ else {
45
+ element = doc.createElement(type);
46
+ }
47
+ // Handle ref first (before processing other props)
48
+ let ref = null;
49
+ if (props && props.ref) {
50
+ ref = props.ref;
51
+ }
52
+ // Apply props
53
+ if (props) {
54
+ Object.keys(props).forEach(key => {
55
+ if (key === 'key' || key === 'ref') {
56
+ // Skip these - ref is handled separately
57
+ return;
58
+ }
59
+ const value = props[key];
60
+ if (value === null || value === undefined) {
61
+ return;
62
+ }
63
+ // For SVG elements, always use setAttribute
64
+ if (element instanceof SVGElement) {
65
+ // Style support (including Subscribable values)
66
+ if (key === 'style' && typeof value === 'object') {
67
+ Object.keys(value).forEach(styleKey => {
68
+ const styleVal = value[styleKey];
69
+ // Subscribable-like: has get/sub
70
+ if (styleVal && typeof styleVal === 'object' && typeof styleVal.get === 'function' && typeof styleVal.sub === 'function') {
71
+ try {
72
+ element.style[styleKey] = String(styleVal.get());
73
+ }
74
+ catch { /* ignore */ }
75
+ styleVal.sub((v) => {
76
+ try {
77
+ element.style[styleKey] = String(v);
78
+ }
79
+ catch { /* ignore */ }
80
+ });
81
+ }
82
+ else {
83
+ try {
84
+ element.style[styleKey] = String(styleVal);
85
+ }
86
+ catch { /* ignore */ }
87
+ }
88
+ });
89
+ return;
90
+ }
91
+ // Normalize className -> class for SVG
92
+ if (key === 'className') {
93
+ element.setAttribute('class', String(value));
94
+ return;
95
+ }
96
+ if (key === 'class') {
97
+ element.setAttribute('class', String(value));
98
+ return;
99
+ }
100
+ // Normalize some common camelCase SVG attributes
101
+ const svgAttrMap = {
102
+ strokeWidth: 'stroke-width',
103
+ fillRule: 'fill-rule',
104
+ dominantBaseline: 'dominant-baseline',
105
+ textAnchor: 'text-anchor',
106
+ };
107
+ const attrName = svgAttrMap[key] || key.replace(/([A-Z])/g, '-$1').toLowerCase();
108
+ element.setAttribute(attrName, String(value));
109
+ }
110
+ else {
111
+ // For HTML elements
112
+ if (key === 'style' && typeof value === 'object') {
113
+ Object.keys(value).forEach(styleKey => {
114
+ const styleVal = value[styleKey];
115
+ if (styleVal && typeof styleVal === 'object' && typeof styleVal.get === 'function' && typeof styleVal.sub === 'function') {
116
+ try {
117
+ element.style[styleKey] = String(styleVal.get());
118
+ }
119
+ catch { /* ignore */ }
120
+ styleVal.sub((v) => {
121
+ try {
122
+ element.style[styleKey] = String(v);
123
+ }
124
+ catch { /* ignore */ }
125
+ });
126
+ }
127
+ else {
128
+ try {
129
+ element.style[styleKey] = String(styleVal);
130
+ }
131
+ catch { /* ignore */ }
132
+ }
133
+ });
134
+ return;
135
+ }
136
+ if (key === 'className') {
137
+ element.className = String(value);
138
+ }
139
+ else if (key === 'class') {
140
+ element.className = String(value);
141
+ }
142
+ else if (key.startsWith('data-')) {
143
+ element.setAttribute(key, String(value));
144
+ }
145
+ else {
146
+ // Try to set as property first, fallback to attribute
147
+ try {
148
+ element[key] = value;
149
+ }
150
+ catch {
151
+ element.setAttribute(key, String(value));
152
+ }
153
+ }
154
+ }
155
+ });
156
+ }
157
+ // Set ref.instance after element is created and props are applied
158
+ if (ref && typeof ref === 'object' && 'instance' in ref) {
159
+ ref.instance = element;
160
+ }
161
+ // Process children - recursively build VNodes into DOM nodes
162
+ // Children from JSX transformation are already VNodes (results of buildComponent calls)
163
+ const processChildren = (childList) => {
164
+ childList.forEach(child => {
165
+ if (child === null || child === undefined) {
166
+ return; // Skip JSX comments and null children
167
+ }
168
+ // String/number children become text nodes
169
+ if (typeof child === 'string' || typeof child === 'number') {
170
+ element.appendChild(doc.createTextNode(String(child)));
171
+ return;
172
+ }
173
+ if (child && typeof child === 'object') {
174
+ // If child is already a DOM Node, append directly
175
+ if (child instanceof Node) {
176
+ element.appendChild(child);
177
+ return;
178
+ }
179
+ // If child is a VNode with an instance (already built), append the instance
180
+ if (child.instance && child.instance instanceof Node) {
181
+ element.appendChild(child.instance);
182
+ return;
183
+ }
184
+ // If child is a VNode with a type, build it
185
+ if (child.type) {
186
+ // Extract children from VNode, filtering null/undefined (JSX comments)
187
+ const vnodeChildren = [];
188
+ if (Array.isArray(child.children)) {
189
+ vnodeChildren.push(...child.children.filter((c) => c !== null && c !== undefined));
190
+ }
191
+ else if (child.children !== null && child.children !== undefined) {
192
+ vnodeChildren.push(child.children);
193
+ }
194
+ // Build the child VNode - this will create the DOM element
195
+ const built = exports.FSComponent.buildComponent(child.type, child.props || {}, ...vnodeChildren);
196
+ // Append the built element's instance
197
+ if (built && built.instance && built.instance instanceof Node) {
198
+ element.appendChild(built.instance);
199
+ }
200
+ else if (built && built.type) {
201
+ // If still a VNode, recursively process
202
+ const processedChildren = [];
203
+ if (Array.isArray(built.children)) {
204
+ processedChildren.push(...built.children.filter((c) => c !== null && c !== undefined));
205
+ }
206
+ else if (built.children !== null && built.children !== undefined) {
207
+ processedChildren.push(built.children);
208
+ }
209
+ const processed = exports.FSComponent.buildComponent(built.type, built.props || {}, ...processedChildren);
210
+ if (processed && processed.instance && processed.instance instanceof Node) {
211
+ element.appendChild(processed.instance);
212
+ }
213
+ }
214
+ return;
215
+ }
216
+ // If child is an array, process recursively
217
+ if (Array.isArray(child)) {
218
+ processChildren(child);
219
+ return;
220
+ }
221
+ }
222
+ });
223
+ };
224
+ if (children && children.length > 0) {
225
+ processChildren(children);
226
+ }
227
+ return {
228
+ type,
229
+ props,
230
+ children,
231
+ instance: element
232
+ };
233
+ }
234
+ // If it's a function (component), instantiate it
235
+ if (typeof type === 'function') {
236
+ const component = new type(props);
237
+ // If a ref was provided, set it to the component instance (component refs).
238
+ if (props && props.ref && typeof props.ref === 'object' && 'instance' in props.ref) {
239
+ props.ref.instance = component;
240
+ }
241
+ const renderResult = component.render();
242
+ if (renderResult) {
243
+ return renderResult;
244
+ }
245
+ return { type, props, children, instance: null };
246
+ }
247
+ return { type, props, children };
248
+ },
249
+ render: (vnode, container) => {
250
+ if (!vnode)
251
+ return;
252
+ const targetDoc = container.ownerDocument || globalThis.document;
253
+ if (!targetDoc)
254
+ return;
255
+ // Helper to safely adopt or clone a node into the target document
256
+ const adoptOrCloneNode = (node) => {
257
+ // Check if node is already in the target document
258
+ if (node.ownerDocument === targetDoc) {
259
+ return node;
260
+ }
261
+ // Try to adopt the node (works in real browsers, may not work in jsdom)
262
+ try {
263
+ if (targetDoc.adoptNode) {
264
+ return targetDoc.adoptNode(node);
265
+ }
266
+ }
267
+ catch (e) {
268
+ // If adoptNode fails, clone the node
269
+ }
270
+ // Clone the node and its children recursively
271
+ return node.cloneNode(true);
272
+ };
273
+ // Helper to safely append a node to a parent
274
+ const safeAppendChild = (parent, child) => {
275
+ try {
276
+ parent.appendChild(child);
277
+ }
278
+ catch (e) {
279
+ // If appendChild fails (e.g., cross-document issue), try to adopt/clone first
280
+ const adoptedChild = adoptOrCloneNode(child);
281
+ parent.appendChild(adoptedChild);
282
+ }
283
+ };
284
+ // Helper to re-establish refs after node manipulation
285
+ // This ensures refs point to the correct DOM nodes after cloning/adopting
286
+ const reestablishRefs = (vnode, domNode) => {
287
+ if (!vnode || !domNode)
288
+ return;
289
+ // Update VNode instance to point to the new DOM node
290
+ vnode.instance = domNode;
291
+ // If this VNode has a ref in props, update it to point to the DOM node
292
+ if (vnode.props && vnode.props.ref && typeof vnode.props.ref === 'object' && 'instance' in vnode.props.ref) {
293
+ vnode.props.ref.instance = domNode;
294
+ }
295
+ // Also check if the built VNode has refs that need updating
296
+ if (vnode.instance === domNode && vnode.props && vnode.props.ref) {
297
+ // This is the original instance, ref should already be set, but ensure it's correct
298
+ if (typeof vnode.props.ref === 'object' && 'instance' in vnode.props.ref) {
299
+ vnode.props.ref.instance = domNode;
300
+ }
301
+ }
302
+ // Recursively process children - match VNode children with DOM child nodes
303
+ if (vnode.children && domNode instanceof Element) {
304
+ const childNodes = Array.from(domNode.childNodes).filter(n => n instanceof Element);
305
+ const vnodeChildren = Array.isArray(vnode.children)
306
+ ? vnode.children.filter((c) => c && (c.type || c.instance))
307
+ : (vnode.children && (vnode.children.type || vnode.children.instance) ? [vnode.children] : []);
308
+ // Match VNode children with DOM nodes by position
309
+ vnodeChildren.forEach((childVNode, vnodeIndex) => {
310
+ if (!childVNode)
311
+ return;
312
+ // Find corresponding DOM node
313
+ // For elements, try to match by type/position
314
+ let domChild = null;
315
+ if (childVNode.instance && childVNode.instance instanceof Node) {
316
+ // VNode has an instance - find it in the DOM tree
317
+ for (const node of childNodes) {
318
+ if (node === childVNode.instance ||
319
+ (node instanceof Element && childVNode.instance instanceof Element &&
320
+ node.tagName === childVNode.instance.tagName &&
321
+ node.getAttribute('id') === childVNode.instance.getAttribute('id'))) {
322
+ domChild = node;
323
+ break;
324
+ }
325
+ }
326
+ }
327
+ else if (vnodeIndex < childNodes.length) {
328
+ // Fallback: match by position
329
+ domChild = childNodes[vnodeIndex];
330
+ }
331
+ if (domChild) {
332
+ reestablishRefs(childVNode, domChild);
333
+ }
334
+ });
335
+ }
336
+ };
337
+ // Recursively build and render VNode tree
338
+ const renderVNode = (node) => {
339
+ if (!node)
340
+ return null;
341
+ // If it's already a DOM node, adopt/clone it
342
+ if (node instanceof Node) {
343
+ return adoptOrCloneNode(node);
344
+ }
345
+ // If it has an instance, adopt/clone it and re-establish refs
346
+ if (node.instance && node.instance instanceof Node) {
347
+ const adoptedNode = adoptOrCloneNode(node.instance);
348
+ reestablishRefs(node, adoptedNode);
349
+ return adoptedNode;
350
+ }
351
+ // If it's a string or number, create text node
352
+ if (typeof node === 'string' || typeof node === 'number') {
353
+ return targetDoc.createTextNode(String(node));
354
+ }
355
+ // If it's an array, process each element
356
+ if (Array.isArray(node)) {
357
+ const fragment = targetDoc.createDocumentFragment();
358
+ node.forEach(child => {
359
+ const childNode = renderVNode(child);
360
+ if (childNode) {
361
+ safeAppendChild(fragment, childNode);
362
+ }
363
+ });
364
+ return fragment;
365
+ }
366
+ // Build component from VNode
367
+ if (node.type) {
368
+ // If VNode already has an instance (from initial buildComponent), use it directly
369
+ // This preserves refs that were set during the initial build
370
+ if (node.instance && node.instance instanceof Node) {
371
+ const adoptedNode = adoptOrCloneNode(node.instance);
372
+ // Re-establish refs to point to the adopted/cloned node
373
+ reestablishRefs(node, adoptedNode);
374
+ return adoptedNode;
375
+ }
376
+ // Otherwise, build it fresh (shouldn't happen in normal flow, but handle it)
377
+ // Pass children directly to buildComponent - it will handle VNodes, Nodes, strings, etc.
378
+ // Filter out null/undefined (from JSX comments)
379
+ const children = (node.children || []).filter((child) => child !== null && child !== undefined);
380
+ const built = exports.FSComponent.buildComponent(node.type, node.props || {}, ...children);
381
+ // If built has instance, adopt/clone it and re-establish refs
382
+ if (built && built.instance && built.instance instanceof Node) {
383
+ const adoptedNode = adoptOrCloneNode(built.instance);
384
+ reestablishRefs(built, adoptedNode);
385
+ return adoptedNode;
386
+ }
387
+ // If built is a VNode without instance, recursively process it
388
+ if (built && built.type && !built.instance) {
389
+ return renderVNode(built);
390
+ }
391
+ // If built has children but no instance, process children into a fragment
392
+ if (built && built.children && Array.isArray(built.children)) {
393
+ const fragment = targetDoc.createDocumentFragment();
394
+ built.children.forEach((child) => {
395
+ // If child is already a Node, append it
396
+ if (child instanceof Node) {
397
+ safeAppendChild(fragment, adoptOrCloneNode(child));
398
+ }
399
+ else {
400
+ // Otherwise process as VNode
401
+ const childNode = renderVNode(child);
402
+ if (childNode) {
403
+ safeAppendChild(fragment, childNode);
404
+ }
405
+ }
406
+ });
407
+ return fragment;
408
+ }
409
+ }
410
+ return null;
411
+ };
412
+ const rootNode = renderVNode(vnode);
413
+ if (rootNode) {
414
+ safeAppendChild(container, rootNode);
415
+ // Re-establish all refs in the tree after everything is in the DOM
416
+ // This ensures refs point to the final DOM nodes (after any cloning/adopting)
417
+ reestablishRefs(vnode, rootNode);
418
+ }
419
+ },
420
+ Fragment: (props, ...children) => {
421
+ return { type: 'Fragment', children };
422
+ },
423
+ createRef() {
424
+ return { instance: null };
425
+ },
426
+ };
427
+ // Mock Subject
428
+ class Subject {
429
+ constructor(initialValue) {
430
+ this.subscribers = [];
431
+ this.value = initialValue;
432
+ }
433
+ static create(initialValue) {
434
+ return new Subject(initialValue);
435
+ }
436
+ get() {
437
+ return this.value;
438
+ }
439
+ set(value) {
440
+ this.value = value;
441
+ this.subscribers.forEach(sub => sub(value));
442
+ }
443
+ sub(callback, immediate = false) {
444
+ this.subscribers.push(callback);
445
+ if (immediate) {
446
+ callback(this.value);
447
+ }
448
+ return {
449
+ destroy: () => {
450
+ const index = this.subscribers.indexOf(callback);
451
+ if (index > -1) {
452
+ this.subscribers.splice(index, 1);
453
+ }
454
+ }
455
+ };
456
+ }
457
+ /**
458
+ * Create a mapped Subscribable from this subject.
459
+ * Mimics MSFS SDK Subject.map() enough for UI binding tests.
460
+ */
461
+ map(fn, _equalityFunc) {
462
+ return new MappedSubscribable(this, fn);
463
+ }
464
+ }
465
+ exports.Subject = Subject;
466
+ class MappedSubscribable {
467
+ constructor(source, fn) {
468
+ this.source = source;
469
+ this.fn = fn;
470
+ }
471
+ get() {
472
+ const next = this.fn(this.source.get(), this.prev);
473
+ this.prev = next;
474
+ return next;
475
+ }
476
+ sub(callback, immediate = false) {
477
+ const handle = this.source.sub((v) => {
478
+ const mapped = this.fn(v, this.prev);
479
+ this.prev = mapped;
480
+ callback(mapped);
481
+ }, immediate);
482
+ this.subHandle = handle;
483
+ return { destroy: () => handle.destroy() };
484
+ }
485
+ map(fn, _equalityFunc) {
486
+ return new MappedSubscribable(this, fn);
487
+ }
488
+ destroy() {
489
+ this.subHandle?.destroy();
490
+ this.subHandle = undefined;
491
+ }
492
+ }
493
+ class NumberUnit {
494
+ constructor(number, unit) {
495
+ this.number = number;
496
+ this.unit = unit;
497
+ }
498
+ asUnit(_unit) {
499
+ return this.number;
500
+ }
501
+ }
502
+ exports.NumberUnit = NumberUnit;
503
+ class UnitTypeClass {
504
+ constructor(name) {
505
+ this.name = name;
506
+ }
507
+ createNumber(value) {
508
+ return new NumberUnit(value, this);
509
+ }
510
+ }
511
+ exports.UnitTypeClass = UnitTypeClass;
512
+ exports.UnitType = {
513
+ NMILE: new UnitTypeClass('NMILE'),
514
+ };
515
+ exports.NumberUnitSubject = {
516
+ create: (initial) => Subject.create(initial),
517
+ };
518
+ exports.Vec2Math = {
519
+ create(x = 0, y = 0) {
520
+ return new Float64Array([x, y]);
521
+ }
522
+ };
523
+ exports.Vec2Subject = {
524
+ create: (initial) => Subject.create(initial),
525
+ };
526
+ // -----------------------------
527
+ // Map-system stubs (msfs-sdk side)
528
+ // -----------------------------
529
+ exports.MapSystemKeys = {
530
+ FacilityLoader: 'FacilityLoader',
531
+ Weather: 'Weather',
532
+ OwnAirplaneIcon: 'OwnAirplaneIcon',
533
+ };
534
+ class FacilityRepository {
535
+ static getRepository(_bus) {
536
+ return {};
537
+ }
538
+ }
539
+ exports.FacilityRepository = FacilityRepository;
540
+ class FacilityLoader {
541
+ constructor(_repo) { }
542
+ }
543
+ exports.FacilityLoader = FacilityLoader;
544
+ class MapProjection {
545
+ project(_lla, out) {
546
+ out[0] = 0.5;
547
+ out[1] = 0.5;
548
+ return out;
549
+ }
550
+ }
551
+ exports.MapProjection = MapProjection;
552
+ class BingComponent {
553
+ static createEarthColorsArray(_waterColor, _stops, _a, _b, _c) {
554
+ return [];
555
+ }
556
+ }
557
+ exports.BingComponent = BingComponent;
558
+ class MapIndexedRangeModule {
559
+ constructor() {
560
+ this.nominalRange = Subject.create(exports.UnitType.NMILE.createNumber(100));
561
+ }
562
+ }
563
+ exports.MapIndexedRangeModule = MapIndexedRangeModule;
564
+ class MapOwnAirplaneIconModule {
565
+ }
566
+ exports.MapOwnAirplaneIconModule = MapOwnAirplaneIconModule;
567
+ class MapOwnAirplanePropsModule {
568
+ }
569
+ exports.MapOwnAirplanePropsModule = MapOwnAirplanePropsModule;
570
+ class MapWxrModule {
571
+ }
572
+ exports.MapWxrModule = MapWxrModule;
573
+ class MockMapContext {
574
+ constructor(bus, _modules, _controllers) {
575
+ this.bus = bus;
576
+ this._modules = _modules;
577
+ this._controllers = _controllers;
578
+ this.model = {
579
+ getModule: (key) => {
580
+ var _d;
581
+ return ((_d = this._modules)[key] ?? (_d[key] = {}));
582
+ },
583
+ };
584
+ this.projection = new MapProjection();
585
+ this.projectionChanged = Subject.create(undefined);
586
+ this.bingRef = { instance: { setWxrColors: (_) => { }, wxrColors: Subject.create([]) } };
587
+ }
588
+ getController(key) {
589
+ return this._controllers[key];
590
+ }
591
+ }
592
+ class MockMapSystemBuilder {
593
+ constructor(bus) {
594
+ this.bus = bus;
595
+ this.modules = {};
596
+ this.controllers = {};
597
+ this.rangeValues = [];
598
+ this._inits = [];
599
+ }
600
+ withContext(_key, _factory) { return this; }
601
+ withModule(key, factory) {
602
+ // create module instance eagerly
603
+ this.modules[String(key)] = factory();
604
+ return this;
605
+ }
606
+ with(_token, maybeRangeArray) {
607
+ // Special-case range arrays: passed as second arg by StormScopeMapManager via GarminMapBuilder.range
608
+ if (Array.isArray(maybeRangeArray)) {
609
+ this.rangeValues = maybeRangeArray;
610
+ // ensure Range module has nominalRange
611
+ if (!this.modules['Range']) {
612
+ this.modules['Range'] = new MapIndexedRangeModule();
613
+ }
614
+ }
615
+ return this;
616
+ }
617
+ withInit(_name, init) {
618
+ // run init later at build time
619
+ this._inits.push(init);
620
+ return this;
621
+ }
622
+ withBing(_bingId, _opts) { return this; }
623
+ withOwnAirplanePropBindings(_bindings, _hz) { return this; }
624
+ withFollowAirplane() { return this; }
625
+ withController(key, factory) {
626
+ this.controllers[String(key)] = factory({ model: this.modules, bus: this.bus });
627
+ return this;
628
+ }
629
+ withProjectedSize(_size) { return this; }
630
+ build(mapId) {
631
+ // Ensure Range module shape expected by StormScopeMapManager
632
+ if (!this.modules['Range']) {
633
+ this.modules['Range'] = new MapIndexedRangeModule();
634
+ }
635
+ const rangeModule = this.modules['Range'];
636
+ if (rangeModule && !rangeModule.nominalRange) {
637
+ rangeModule.nominalRange = Subject.create(exports.UnitType.NMILE.createNumber(100));
638
+ }
639
+ // Provide a default Range controller if not present
640
+ if (!this.controllers['Range']) {
641
+ // Lazy require to avoid circular import
642
+ try {
643
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
644
+ const { MapRangeController } = require('@microsoft/msfs-garminsdk');
645
+ this.controllers['Range'] = new MapRangeController(this.rangeValues, rangeModule.nominalRange);
646
+ }
647
+ catch {
648
+ this.controllers['Range'] = { changeRangeIndex: () => { }, setRangeIndex: () => { } };
649
+ }
650
+ }
651
+ const context = new MockMapContext(this.bus, this.modules, this.controllers);
652
+ // Run init hooks with a context-like object
653
+ this._inits.forEach(fn => {
654
+ try {
655
+ fn({ bus: this.bus, model: { getModule: (k) => this.modules[String(k)] }, getController: (k) => this.controllers[String(k)], ...context });
656
+ }
657
+ catch { /* ignore */ }
658
+ });
659
+ const mapVNode = exports.FSComponent.buildComponent('div', { id: mapId, class: 'stormscope-map' });
660
+ const ref = { instance: { update: (_t) => { }, wake: () => { }, sleep: () => { } } };
661
+ return { context, map: mapVNode, ref };
662
+ }
663
+ }
664
+ exports.MapSystemBuilder = {
665
+ create: (bus) => new MockMapSystemBuilder(bus),
666
+ };
667
+ // Utility namespace placeholder used by some code imports
668
+ exports.SubscribableUtils = {};
669
+ // Mock EventBus
670
+ class EventBus {
671
+ constructor() {
672
+ this.events = new Map();
673
+ }
674
+ on(topic, callback) {
675
+ if (!this.events.has(topic)) {
676
+ this.events.set(topic, []);
677
+ }
678
+ this.events.get(topic).push(callback);
679
+ }
680
+ off(topic, callback) {
681
+ const callbacks = this.events.get(topic);
682
+ if (callbacks) {
683
+ const index = callbacks.indexOf(callback);
684
+ if (index > -1) {
685
+ callbacks.splice(index, 1);
686
+ }
687
+ }
688
+ }
689
+ pub(topic, data) {
690
+ const callbacks = this.events.get(topic);
691
+ if (callbacks) {
692
+ callbacks.forEach(cb => cb(data));
693
+ }
694
+ }
695
+ }
696
+ exports.EventBus = EventBus;