@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,514 @@
1
+ "use strict";
2
+ /**
3
+ * Mock MSFS global objects and types required by SDK
4
+ *
5
+ * These must be set on global object BEFORE any SDK code loads
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.setupMSFSGlobals = setupMSFSGlobals;
9
+ function setupMSFSGlobals() {
10
+ const globalObj = globalThis;
11
+ // BaseInstrument stub
12
+ if (typeof globalObj.BaseInstrument === 'undefined') {
13
+ globalObj.BaseInstrument = class BaseInstrument {
14
+ getChildById(id) {
15
+ return null;
16
+ }
17
+ };
18
+ }
19
+ // DisplayComponent - must be available globally for SDK classes that extend it
20
+ if (typeof globalObj.DisplayComponent === 'undefined') {
21
+ globalObj.DisplayComponent = class DisplayComponent {
22
+ constructor(props) {
23
+ this.props = props;
24
+ }
25
+ render() {
26
+ return null;
27
+ }
28
+ onAfterRender(_vnode) {
29
+ // Default implementation
30
+ }
31
+ destroy() {
32
+ // Default implementation
33
+ }
34
+ };
35
+ }
36
+ // FSComponent - must be available globally for JSX factory
37
+ // This implementation creates actual DOM elements for testing
38
+ if (typeof globalObj.FSComponent === 'undefined') {
39
+ globalObj.FSComponent = {
40
+ buildComponent: (type, props, ...children) => {
41
+ // If it's a string (HTML/SVG tag), create a VNode structure with DOM element
42
+ if (typeof type === 'string') {
43
+ const doc = globalThis.document;
44
+ if (!doc) {
45
+ return { type, props, children };
46
+ }
47
+ // Create actual DOM element
48
+ let element;
49
+ 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)) {
50
+ element = doc.createElementNS('http://www.w3.org/2000/svg', type);
51
+ }
52
+ else {
53
+ element = doc.createElement(type);
54
+ }
55
+ // Handle ref first (before processing other props)
56
+ let ref = null;
57
+ if (props && props.ref) {
58
+ ref = props.ref;
59
+ }
60
+ // Apply props
61
+ if (props) {
62
+ Object.keys(props).forEach(key => {
63
+ if (key === 'key' || key === 'ref') {
64
+ // Skip these - ref is handled separately
65
+ return;
66
+ }
67
+ const value = props[key];
68
+ if (value === null || value === undefined) {
69
+ return;
70
+ }
71
+ // For SVG elements, always use setAttribute
72
+ if (element instanceof SVGElement) {
73
+ // Style object support (including Subscribable values)
74
+ if (key === 'style' && typeof value === 'object') {
75
+ Object.keys(value).forEach(styleKey => {
76
+ const styleVal = value[styleKey];
77
+ if (styleVal && typeof styleVal === 'object' && typeof styleVal.get === 'function' && typeof styleVal.sub === 'function') {
78
+ try {
79
+ element.style[styleKey] = String(styleVal.get());
80
+ }
81
+ catch { }
82
+ styleVal.sub((v) => { try {
83
+ element.style[styleKey] = String(v);
84
+ }
85
+ catch { } });
86
+ }
87
+ else {
88
+ try {
89
+ element.style[styleKey] = String(styleVal);
90
+ }
91
+ catch { }
92
+ }
93
+ });
94
+ return;
95
+ }
96
+ if (key === 'className') {
97
+ element.setAttribute('class', String(value));
98
+ return;
99
+ }
100
+ if (key === 'class') {
101
+ element.setAttribute('class', String(value));
102
+ return;
103
+ }
104
+ const svgAttrMap = {
105
+ strokeWidth: 'stroke-width',
106
+ fillRule: 'fill-rule',
107
+ dominantBaseline: 'dominant-baseline',
108
+ textAnchor: 'text-anchor',
109
+ };
110
+ const attrName = svgAttrMap[key] || key.replace(/([A-Z])/g, '-$1').toLowerCase();
111
+ element.setAttribute(attrName, String(value));
112
+ }
113
+ else {
114
+ // For HTML elements
115
+ if (key === 'style' && typeof value === 'object') {
116
+ Object.keys(value).forEach(styleKey => {
117
+ const styleVal = value[styleKey];
118
+ if (styleVal && typeof styleVal === 'object' && typeof styleVal.get === 'function' && typeof styleVal.sub === 'function') {
119
+ try {
120
+ element.style[styleKey] = String(styleVal.get());
121
+ }
122
+ catch { }
123
+ styleVal.sub((v) => { try {
124
+ element.style[styleKey] = String(v);
125
+ }
126
+ catch { } });
127
+ }
128
+ else {
129
+ try {
130
+ element.style[styleKey] = String(styleVal);
131
+ }
132
+ catch { }
133
+ }
134
+ });
135
+ return;
136
+ }
137
+ if (key === 'className') {
138
+ element.className = String(value);
139
+ }
140
+ else if (key === 'class') {
141
+ element.className = String(value);
142
+ }
143
+ else if (key.startsWith('data-')) {
144
+ element.setAttribute(key, String(value));
145
+ }
146
+ else {
147
+ // Try to set as property first, fallback to attribute
148
+ try {
149
+ element[key] = value;
150
+ }
151
+ catch {
152
+ element.setAttribute(key, String(value));
153
+ }
154
+ }
155
+ }
156
+ });
157
+ }
158
+ // Set ref.instance after element is created and props are applied
159
+ if (ref && typeof ref === 'object' && 'instance' in ref) {
160
+ ref.instance = element;
161
+ }
162
+ // Helper to safely append a node
163
+ const safeAppendChild = (parent, child) => {
164
+ try {
165
+ parent.appendChild(child);
166
+ }
167
+ catch (e) {
168
+ // If appendChild fails (e.g., cross-document issue), try to adopt/clone first
169
+ try {
170
+ if (doc.adoptNode) {
171
+ parent.appendChild(doc.adoptNode(child));
172
+ }
173
+ else {
174
+ parent.appendChild(child.cloneNode(true));
175
+ }
176
+ }
177
+ catch (e2) {
178
+ // If all else fails, clone
179
+ parent.appendChild(child.cloneNode(true));
180
+ }
181
+ }
182
+ };
183
+ // Process children - recursively build VNodes into DOM nodes
184
+ const processChildren = (childList) => {
185
+ childList.forEach(child => {
186
+ if (child === null || child === undefined) {
187
+ return; // Skip JSX comments and null children
188
+ }
189
+ // String/number children become text nodes
190
+ if (typeof child === 'string' || typeof child === 'number') {
191
+ element.appendChild(doc.createTextNode(String(child)));
192
+ return;
193
+ }
194
+ if (child && typeof child === 'object') {
195
+ // If child is already a DOM Node, append safely
196
+ if (child instanceof Node) {
197
+ safeAppendChild(element, child);
198
+ return;
199
+ }
200
+ // If child is a VNode with an instance (already built), append the instance
201
+ if (child.instance && child.instance instanceof Node) {
202
+ safeAppendChild(element, child.instance);
203
+ return;
204
+ }
205
+ // If child is a VNode with a type, build it
206
+ if (child.type) {
207
+ // Extract children from VNode, filtering null/undefined (JSX comments)
208
+ const vnodeChildren = [];
209
+ if (Array.isArray(child.children)) {
210
+ vnodeChildren.push(...child.children.filter((c) => c !== null && c !== undefined));
211
+ }
212
+ else if (child.children !== null && child.children !== undefined) {
213
+ vnodeChildren.push(child.children);
214
+ }
215
+ // Build the child VNode - this will create the DOM element
216
+ const built = globalObj.FSComponent.buildComponent(child.type, child.props || {}, ...vnodeChildren);
217
+ // Append the built element's instance
218
+ if (built && built.instance && built.instance instanceof Node) {
219
+ safeAppendChild(element, built.instance);
220
+ }
221
+ else if (built && built.type) {
222
+ // If still a VNode, recursively process
223
+ const processedChildren = [];
224
+ if (Array.isArray(built.children)) {
225
+ processedChildren.push(...built.children.filter((c) => c !== null && c !== undefined));
226
+ }
227
+ else if (built.children !== null && built.children !== undefined) {
228
+ processedChildren.push(built.children);
229
+ }
230
+ const processed = globalObj.FSComponent.buildComponent(built.type, built.props || {}, ...processedChildren);
231
+ if (processed && processed.instance && processed.instance instanceof Node) {
232
+ safeAppendChild(element, processed.instance);
233
+ }
234
+ }
235
+ return;
236
+ }
237
+ // If child is an array, process recursively
238
+ if (Array.isArray(child)) {
239
+ processChildren(child);
240
+ return;
241
+ }
242
+ }
243
+ });
244
+ };
245
+ if (children && children.length > 0) {
246
+ processChildren(children);
247
+ }
248
+ return {
249
+ type,
250
+ props,
251
+ children,
252
+ instance: element
253
+ };
254
+ }
255
+ // If it's a function (component), instantiate it
256
+ if (typeof type === 'function') {
257
+ const component = new type(props);
258
+ // Component refs: set ref.instance to the component instance
259
+ if (props && props.ref && typeof props.ref === 'object' && 'instance' in props.ref) {
260
+ props.ref.instance = component;
261
+ }
262
+ const renderResult = component.render();
263
+ if (renderResult) {
264
+ return renderResult;
265
+ }
266
+ return { type, props, children, instance: null };
267
+ }
268
+ return { type, props, children };
269
+ },
270
+ render: (vnode, container) => {
271
+ if (!vnode)
272
+ return;
273
+ const targetDoc = container.ownerDocument || globalThis.document;
274
+ if (!targetDoc)
275
+ return;
276
+ // Temporarily set global document to target document so buildComponent uses it
277
+ const originalDoc = globalThis.document;
278
+ globalThis.document = targetDoc;
279
+ // Helper to recreate a node in the target document
280
+ const recreateNodeInDocument = (node, doc) => {
281
+ // Check if node is already in the target document
282
+ if (node.ownerDocument === doc) {
283
+ return node;
284
+ }
285
+ // Try to adopt the node first (works in real browsers, may not work in jsdom)
286
+ try {
287
+ if (doc.adoptNode) {
288
+ return doc.adoptNode(node);
289
+ }
290
+ }
291
+ catch (e) {
292
+ // If adoptNode fails, recreate the node
293
+ }
294
+ // Recreate the element in the target document
295
+ if (node instanceof Element) {
296
+ const tagName = node.tagName.toLowerCase();
297
+ let newElement;
298
+ // Check if it's an SVG element
299
+ if (node instanceof SVGElement || node.namespaceURI === 'http://www.w3.org/2000/svg') {
300
+ newElement = doc.createElementNS('http://www.w3.org/2000/svg', tagName);
301
+ }
302
+ else {
303
+ newElement = doc.createElement(tagName);
304
+ }
305
+ // Copy attributes
306
+ Array.from(node.attributes).forEach(attr => {
307
+ newElement.setAttribute(attr.name, attr.value);
308
+ });
309
+ // Recursively recreate children
310
+ Array.from(node.childNodes).forEach(child => {
311
+ const recreatedChild = recreateNodeInDocument(child, doc);
312
+ newElement.appendChild(recreatedChild);
313
+ });
314
+ return newElement;
315
+ }
316
+ else if (node instanceof Text) {
317
+ return doc.createTextNode(node.textContent || '');
318
+ }
319
+ else if (node instanceof DocumentFragment) {
320
+ const fragment = doc.createDocumentFragment();
321
+ Array.from(node.childNodes).forEach(child => {
322
+ const recreatedChild = recreateNodeInDocument(child, doc);
323
+ fragment.appendChild(recreatedChild);
324
+ });
325
+ return fragment;
326
+ }
327
+ // Fallback: try clone
328
+ return node.cloneNode(true);
329
+ };
330
+ // Helper to safely append a node to a parent
331
+ const safeAppendChild = (parent, child) => {
332
+ try {
333
+ // Check if child is in the same document
334
+ if (child.ownerDocument !== targetDoc && child instanceof Node) {
335
+ const recreated = recreateNodeInDocument(child, targetDoc);
336
+ parent.appendChild(recreated);
337
+ }
338
+ else {
339
+ parent.appendChild(child);
340
+ }
341
+ }
342
+ catch (e) {
343
+ // If appendChild still fails, try recreating
344
+ try {
345
+ const recreated = recreateNodeInDocument(child, targetDoc);
346
+ parent.appendChild(recreated);
347
+ }
348
+ catch (e2) {
349
+ // Last resort: log and skip
350
+ console.warn('Failed to append node:', e2);
351
+ }
352
+ }
353
+ };
354
+ // Recursively build and render VNode tree
355
+ const renderVNode = (node) => {
356
+ if (!node)
357
+ return null;
358
+ // If it's already a DOM node, recreate it in target document
359
+ if (node instanceof Node) {
360
+ return recreateNodeInDocument(node, targetDoc);
361
+ }
362
+ // If it has an instance, recreate it in target document
363
+ if (node.instance && node.instance instanceof Node) {
364
+ return recreateNodeInDocument(node.instance, targetDoc);
365
+ }
366
+ // If it's a string or number, create text node
367
+ if (typeof node === 'string' || typeof node === 'number') {
368
+ return targetDoc.createTextNode(String(node));
369
+ }
370
+ // If it's an array, process each element
371
+ if (Array.isArray(node)) {
372
+ const fragment = targetDoc.createDocumentFragment();
373
+ node.forEach(child => {
374
+ const childNode = renderVNode(child);
375
+ if (childNode) {
376
+ safeAppendChild(fragment, childNode);
377
+ }
378
+ });
379
+ return fragment;
380
+ }
381
+ // Build component from VNode
382
+ if (node.type) {
383
+ // Pass children directly to buildComponent - it will handle VNodes, Nodes, strings, etc.
384
+ // Filter out null/undefined (from JSX comments)
385
+ const children = (node.children || []).filter((child) => child !== null && child !== undefined);
386
+ const built = globalObj.FSComponent.buildComponent(node.type, node.props || {}, ...children);
387
+ // If built has instance, recreate it in target document
388
+ if (built && built.instance && built.instance instanceof Node) {
389
+ return recreateNodeInDocument(built.instance, targetDoc);
390
+ }
391
+ // If built is a VNode without instance, recursively process it
392
+ if (built && built.type && !built.instance) {
393
+ return renderVNode(built);
394
+ }
395
+ // If built has children but no instance, process children into a fragment
396
+ if (built && built.children && Array.isArray(built.children)) {
397
+ const fragment = targetDoc.createDocumentFragment();
398
+ built.children.forEach((child) => {
399
+ // If child is already a Node, append it
400
+ if (child instanceof Node) {
401
+ safeAppendChild(fragment, recreateNodeInDocument(child, targetDoc));
402
+ }
403
+ else {
404
+ // Otherwise process as VNode
405
+ const childNode = renderVNode(child);
406
+ if (childNode) {
407
+ safeAppendChild(fragment, childNode);
408
+ }
409
+ }
410
+ });
411
+ return fragment;
412
+ }
413
+ }
414
+ return null;
415
+ };
416
+ // Helper to re-establish refs after node recreation
417
+ const reestablishRefs = (vnode, domNode) => {
418
+ if (!vnode || !domNode)
419
+ return;
420
+ // Update VNode instance to point to the new DOM node
421
+ vnode.instance = domNode;
422
+ // If this VNode has a ref in props, update it to point to the DOM node
423
+ if (vnode.props && vnode.props.ref && typeof vnode.props.ref === 'object' && 'instance' in vnode.props.ref) {
424
+ vnode.props.ref.instance = domNode;
425
+ }
426
+ // Recursively process children - match VNode children to DOM children
427
+ if (vnode.children && Array.isArray(vnode.children) && domNode instanceof Element) {
428
+ // Get all element children from DOM (skip text nodes)
429
+ const domChildren = Array.from(domNode.childNodes).filter(n => n instanceof Element);
430
+ let domIndex = 0;
431
+ vnode.children.forEach((childVNode) => {
432
+ if (!childVNode)
433
+ return;
434
+ // Skip non-VNode children (strings, numbers, nulls)
435
+ if (typeof childVNode !== 'object')
436
+ return;
437
+ // Process VNode children
438
+ if (childVNode.type || childVNode.instance) {
439
+ if (domIndex < domChildren.length) {
440
+ reestablishRefs(childVNode, domChildren[domIndex]);
441
+ domIndex++;
442
+ }
443
+ }
444
+ });
445
+ }
446
+ };
447
+ const rootNode = renderVNode(vnode);
448
+ if (rootNode) {
449
+ safeAppendChild(container, rootNode);
450
+ // Re-establish refs after all nodes are in the DOM
451
+ reestablishRefs(vnode, rootNode);
452
+ }
453
+ // Restore original document
454
+ globalThis.document = originalDoc;
455
+ },
456
+ Fragment: (props, ...children) => {
457
+ return { type: 'Fragment', children };
458
+ },
459
+ createRef() {
460
+ return { instance: null };
461
+ },
462
+ };
463
+ }
464
+ // GameState enum
465
+ if (typeof globalObj.GameState === 'undefined') {
466
+ globalObj.GameState = {
467
+ NONE: 0,
468
+ MENU: 1,
469
+ LOADING: 2,
470
+ FLYING: 3,
471
+ PAUSED: 4,
472
+ };
473
+ }
474
+ // Name_Z type (string alias in MSFS)
475
+ if (typeof globalObj.Name_Z === 'undefined') {
476
+ globalObj.Name_Z = String;
477
+ }
478
+ // RunwayDesignator
479
+ if (typeof globalObj.RunwayDesignator === 'undefined') {
480
+ globalObj.RunwayDesignator = {
481
+ NONE: 0,
482
+ LEFT: 1,
483
+ RIGHT: 2,
484
+ CENTER: 3,
485
+ };
486
+ }
487
+ // AirportClass
488
+ if (typeof globalObj.AirportClass === 'undefined') {
489
+ globalObj.AirportClass = {
490
+ HELIPORT: 0,
491
+ SMALL_AIRPORT: 1,
492
+ MEDIUM_AIRPORT: 2,
493
+ LARGE_AIRPORT: 3,
494
+ };
495
+ }
496
+ // Avionics global
497
+ if (typeof globalObj.Avionics === 'undefined') {
498
+ globalObj.Avionics = {
499
+ getCurrentGpsTime: () => Date.now(),
500
+ getCurrentUtcTime: () => Date.now(),
501
+ Utils: {
502
+ DEG2RAD: Math.PI / 180,
503
+ RAD2DEG: 180 / Math.PI,
504
+ },
505
+ };
506
+ }
507
+ // Storage functions
508
+ if (typeof globalObj.GetStoredData === 'undefined') {
509
+ globalObj.GetStoredData = () => '';
510
+ }
511
+ if (typeof globalObj.SetStoredData === 'undefined') {
512
+ globalObj.SetStoredData = () => { };
513
+ }
514
+ }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * SDK Adapter for Jest testing
3
+ *
4
+ * Provides mock implementations of MSFS SDK classes for testing.
5
+ * This allows components to be tested without the full SDK bundle.
6
+ *
7
+ * NOTE: We don't re-export types from @microsoft/msfs-types because
8
+ * they are declaration files and not modules. Types are resolved
9
+ * through TypeScript's type resolution.
10
+ */
11
+ export declare abstract class DisplayComponent<P = any, S = any> {
12
+ props: P;
13
+ state?: S;
14
+ constructor(props: P);
15
+ onBeforeRender?(): void;
16
+ abstract render(): any;
17
+ onAfterRender(vnode?: any): void;
18
+ destroy(): void;
19
+ }
20
+ type FSComponentLike = {
21
+ buildComponent: (type: any, props: any, ...children: any[]) => any;
22
+ render: (vnode: any, container: HTMLElement) => void;
23
+ Fragment: (props: any, ...children: any[]) => any;
24
+ createRef: <T = any>() => {
25
+ instance: T | null;
26
+ };
27
+ };
28
+ export declare const FSComponent: FSComponentLike;
29
+ export interface VNode {
30
+ instance?: any;
31
+ children?: VNode[];
32
+ [key: string]: any;
33
+ }
34
+ export declare class Subject<T> {
35
+ private value;
36
+ private subscribers;
37
+ constructor(initialValue: T);
38
+ static create<T>(initialValue: T): Subject<T>;
39
+ get(): T;
40
+ set(value: T): void;
41
+ sub(callback: (value: T) => void, immediate?: boolean): {
42
+ destroy: () => void;
43
+ };
44
+ /**
45
+ * Create a mapped Subscribable from this subject.
46
+ * Mimics MSFS SDK Subject.map() enough for UI binding tests.
47
+ */
48
+ map<M>(fn: (input: T, previousVal?: M) => M, _equalityFunc?: ((a: M, b: M) => boolean)): Subscribable<M> & Subscription;
49
+ }
50
+ export interface Subscribable<T> {
51
+ get(): T;
52
+ sub(callback: (value: T) => void, immediate?: boolean): {
53
+ destroy: () => void;
54
+ };
55
+ map<M>(fn: (input: T, previousVal?: M) => M, equalityFunc?: ((a: M, b: M) => boolean)): Subscribable<M> & Subscription;
56
+ }
57
+ export type ReadonlyFloat64Array = Readonly<Float64Array>;
58
+ export declare class NumberUnit<U = any> {
59
+ readonly number: number;
60
+ readonly unit: U;
61
+ constructor(number: number, unit: U);
62
+ asUnit(_unit: any): number;
63
+ }
64
+ export declare class UnitTypeClass {
65
+ readonly name: string;
66
+ constructor(name: string);
67
+ createNumber(value: number): NumberUnit<UnitTypeClass>;
68
+ }
69
+ export declare const UnitType: {
70
+ readonly NMILE: UnitTypeClass;
71
+ };
72
+ export type NumberUnitSubject = Subject<NumberUnit<any>>;
73
+ export declare const NumberUnitSubject: {
74
+ readonly create: (initial: NumberUnit<any>) => Subject<NumberUnit<any>>;
75
+ };
76
+ export declare const Vec2Math: {
77
+ create(x?: number, y?: number): Float64Array;
78
+ };
79
+ export type Vec2Subject = Subject<Float64Array>;
80
+ export declare const Vec2Subject: {
81
+ readonly create: (initial: Float64Array) => Subject<Float64Array>;
82
+ };
83
+ export declare const MapSystemKeys: {
84
+ readonly FacilityLoader: "FacilityLoader";
85
+ readonly Weather: "Weather";
86
+ readonly OwnAirplaneIcon: "OwnAirplaneIcon";
87
+ };
88
+ export declare class FacilityRepository {
89
+ static getRepository(_bus: any): any;
90
+ }
91
+ export declare class FacilityLoader {
92
+ constructor(_repo: any);
93
+ }
94
+ export declare class MapProjection {
95
+ project(_lla: {
96
+ lat: number;
97
+ lon: number;
98
+ }, out: Float64Array): Float64Array;
99
+ }
100
+ export declare class BingComponent {
101
+ static createEarthColorsArray(_waterColor: string, _stops: any[], _a: number, _b: number, _c: number): any[];
102
+ }
103
+ export declare class MapIndexedRangeModule {
104
+ readonly nominalRange: Subject<NumberUnit<UnitTypeClass>>;
105
+ }
106
+ export declare class MapOwnAirplaneIconModule {
107
+ }
108
+ export declare class MapOwnAirplanePropsModule {
109
+ }
110
+ export declare class MapWxrModule {
111
+ }
112
+ export type CompiledMapSystem<T = any, U = any, V = any, W = any> = any;
113
+ export declare const MapSystemBuilder: any;
114
+ export declare const SubscribableUtils: any;
115
+ export declare class EventBus {
116
+ private events;
117
+ on<T>(topic: string, callback: (data: T) => void): void;
118
+ off<T>(topic: string, callback: (data: T) => void): void;
119
+ pub<T>(topic: string, data: T): void;
120
+ }
121
+ export interface Subscription {
122
+ destroy(): void;
123
+ }
124
+ export type ComponentProps = any;
125
+ export type DisplayChildren = any;
126
+ export {};