@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.
- package/LICENSE +22 -0
- package/README.md +170 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +24 -0
- package/dist/mocks/CoherentMock.d.ts +74 -0
- package/dist/mocks/CoherentMock.js +148 -0
- package/dist/mocks/GarminSDKAdapter.d.ts +79 -0
- package/dist/mocks/GarminSDKAdapter.js +127 -0
- package/dist/mocks/MSFSGlobals.d.ts +6 -0
- package/dist/mocks/MSFSGlobals.js +514 -0
- package/dist/mocks/SDKAdapter.d.ts +126 -0
- package/dist/mocks/SDKAdapter.js +696 -0
- package/dist/mocks/SDKClasses.d.ts +32 -0
- package/dist/mocks/SDKClasses.js +118 -0
- package/dist/mocks/SimVarMock.d.ts +97 -0
- package/dist/mocks/SimVarMock.js +215 -0
- package/dist/mocks/index.d.ts +9 -0
- package/dist/mocks/index.js +25 -0
- package/dist/setupTests.d.ts +13 -0
- package/dist/setupTests.js +109 -0
- package/dist/test-utils/ComponentTestHelper.d.ts +73 -0
- package/dist/test-utils/ComponentTestHelper.js +558 -0
- package/dist/test-utils/ObservableTestHelper.d.ts +25 -0
- package/dist/test-utils/ObservableTestHelper.js +75 -0
- package/dist/test-utils/TestEnvironment.d.ts +76 -0
- package/dist/test-utils/TestEnvironment.js +227 -0
- package/dist/test-utils/index.d.ts +6 -0
- package/dist/test-utils/index.js +22 -0
- package/package.json +64 -0
|
@@ -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;
|