• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rokucommunity / brs / #84

04 Apr 2024 10:25PM UTC coverage: 89.562% (-0.07%) from 89.636%
#84

push

web-flow
Implement `ifArraySizeInfo` in `roArray` (#62)

* Implemented `ifArraySizeInfo` in `roArray`

* Removing empty line

* Fixed warning message

2024 of 2438 branches covered (83.02%)

Branch coverage included in aggregate %.

92 of 108 new or added lines in 7 files covered. (85.19%)

45 existing lines in 4 files now uncovered.

5801 of 6299 relevant lines covered (92.09%)

29750.15 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.69
/src/brsTypes/components/RoSGNode.ts
1
import {
142✔
2
    BrsValue,
3
    ValueKind,
4
    BrsString,
5
    BrsInvalid,
6
    BrsBoolean,
7
    Uninitialized,
8
    getBrsValueFromFieldType,
9
    getValueKindFromFieldType,
10
} from "../BrsType";
11
import { RoSGNodeEvent } from "./RoSGNodeEvent";
142✔
12
import { BrsComponent, BrsIterable } from "./BrsComponent";
142✔
13
import { BrsType, isBrsNumber } from "..";
142✔
14
import { Callable, StdlibArgument } from "../Callable";
142✔
15
import { Interpreter } from "../../interpreter";
16
import { Int32 } from "../Int32";
142✔
17
import { Int64 } from "../Int64";
142✔
18
import { Float } from "../Float";
142✔
19
import { Double } from "../Double";
142✔
20
import { RoAssociativeArray } from "./RoAssociativeArray";
142✔
21
import { RoArray } from "./RoArray";
142✔
22
import { AAMember } from "./RoAssociativeArray";
23
import { ComponentDefinition, ComponentNode } from "../../componentprocessor";
24
import { ComponentFactory, BrsComponentName } from "./ComponentFactory";
142✔
25
import { Environment } from "../../interpreter/Environment";
26
import { roInvalid } from "./RoInvalid";
142✔
27
import type * as MockNodeModule from "../../extensions/MockNode";
28
import { BlockEnd } from "../../parser/Statement";
142✔
29
import { Stmt } from "../../parser";
142✔
30
import { generateArgumentMismatchError } from "../../interpreter/ArgumentMismatch";
142✔
31

32
interface BrsCallback {
33
    interpreter: Interpreter;
34
    environment: Environment;
35
    hostNode: RoSGNode;
36
    callable: Callable;
37
    eventParams: {
38
        fieldName: BrsString;
39
        node: RoSGNode;
40
    };
41
}
42

43
/** Set of value types that a field could be. */
44
enum FieldKind {
142✔
45
    Interface = "interface",
142✔
46
    Array = "array",
142✔
47
    AssocArray = "assocarray",
142✔
48
    Int32 = "integer",
142✔
49
    Int64 = "longinteger",
142✔
50
    Double = "double",
142✔
51
    Float = "float",
142✔
52
    Node = "node",
142✔
53
    Boolean = "boolean",
142✔
54
    String = "string",
142✔
55
    Function = "function",
142✔
56
}
57

58
namespace FieldKind {
142!
59
    export function fromString(type: string): FieldKind | undefined {
142✔
60
        switch (type.toLowerCase()) {
11,951!
61
            case "interface":
62
                return FieldKind.Interface;
×
63
            case "array":
64
            case "roarray":
65
                return FieldKind.Array;
1,018✔
66
            case "roassociativearray":
67
            case "assocarray":
68
                return FieldKind.AssocArray;
1,189✔
69
            case "node":
70
                return FieldKind.Node;
1,033✔
71
            case "bool":
72
            case "boolean":
73
                return FieldKind.Boolean;
2,168✔
74
            case "int":
75
            case "integer":
76
                return FieldKind.Int32;
1,007✔
77
            case "longint":
78
            case "longinteger":
79
                return FieldKind.Int64;
1✔
80
            case "float":
81
                return FieldKind.Float;
899✔
82
            case "double":
83
                return FieldKind.Double;
4✔
84
            case "uri":
85
            case "str":
86
            case "string":
87
                return FieldKind.String;
4,585✔
88
            case "function":
89
                return FieldKind.Function;
2✔
90
            default:
91
                return undefined;
45✔
92
        }
93
    }
94

95
    export function fromBrsType(brsType: BrsType): FieldKind | undefined {
142✔
96
        if (brsType.kind !== ValueKind.Object) {
1,693✔
97
            return fromString(ValueKind.toString(brsType.kind));
1,561✔
98
        }
99

100
        let componentName = brsType.getComponentName();
132✔
101
        switch (componentName.toLowerCase()) {
132!
102
            case "roarray":
103
                return FieldKind.Array;
6✔
104
            case "roassociativearray":
105
                return FieldKind.AssocArray;
4✔
106
            case "node":
107
                return FieldKind.Node;
122✔
108
            default:
109
                return undefined;
×
110
        }
111
    }
112
}
113

114
/** This is used to define a field (usually a default/built-in field in a component definition). */
115
export type FieldModel = {
116
    name: string;
117
    type: string;
118
    value?: string;
119
    hidden?: boolean;
120
    alwaysNotify?: boolean;
121
};
122

123
export class Field {
142✔
124
    private permanentObservers: BrsCallback[] = [];
10,947✔
125
    private unscopedObservers: BrsCallback[] = [];
10,947✔
126
    private scopedObservers: Map<RoSGNode, BrsCallback[]> = new Map();
10,947✔
127

128
    constructor(
129
        private value: BrsType,
10,947✔
130
        private type: FieldKind,
10,947✔
131
        private alwaysNotify: boolean,
10,947✔
132
        private hidden: boolean = false
10,947✔
133
    ) {}
134

135
    toString(parent?: BrsType): string {
136
        return this.value.toString(parent);
437✔
137
    }
138

139
    /**
140
     * Returns whether or not the field is "hidden".
141
     *
142
     * The reason for this is that some fields (content metadata fields) are
143
     * by default "hidden". This means they are accessible on the
144
     * node without an access error, but they don't show up when you print the node.
145
     */
146
    isHidden() {
147
        return this.hidden;
2,722✔
148
    }
149

150
    setHidden(isHidden: boolean) {
151
        this.hidden = isHidden;
2✔
152
    }
153

154
    getType(): FieldKind {
155
        return this.type;
304✔
156
    }
157

158
    getValue(): BrsType {
159
        // Once a field is accessed, it is no longer hidden.
160
        this.hidden = false;
621✔
161

162
        return this.value;
621✔
163
    }
164

165
    setValue(value: BrsType) {
166
        // Once a field is set, it is no longer hidden.
167
        this.hidden = false;
530✔
168

169
        if (isBrsNumber(value) && value.kind !== getValueKindFromFieldType(this.type)) {
530✔
170
            if (this.type === FieldKind.Float) {
10✔
171
                value = new Float(value.getValue());
3✔
172
            } else if (this.type === FieldKind.Int32) {
7✔
173
                value = new Int32(value.getValue());
3✔
174
            } else if (this.type === FieldKind.Int64) {
4✔
175
                value = new Int64(value.getValue());
2✔
176
            } else if (this.type === FieldKind.Double) {
2✔
177
                value = new Double(value.getValue());
2✔
178
            }
179
        }
180

181
        let oldValue = this.value;
530✔
182
        this.value = value;
530✔
183
        if (this.alwaysNotify || oldValue !== value) {
530✔
184
            this.permanentObservers.map(this.executeCallbacks.bind(this));
530✔
185
            this.unscopedObservers.map(this.executeCallbacks.bind(this));
530✔
186
            this.scopedObservers.forEach((callbacks) =>
530✔
187
                callbacks.map(this.executeCallbacks.bind(this))
15✔
188
            );
189
        }
190
    }
191

192
    canAcceptValue(value: BrsType) {
193
        // Objects are allowed to be set to invalid.
194
        let fieldIsObject = getValueKindFromFieldType(this.type) === ValueKind.Object;
876✔
195
        if (fieldIsObject && (value === BrsInvalid.Instance || value instanceof roInvalid)) {
876✔
196
            return true;
37✔
197
        } else if (isBrsNumber(this.value) && isBrsNumber(value)) {
839✔
198
            // can convert between number types
199
            return true;
241✔
200
        }
201

202
        return this.type === FieldKind.fromBrsType(value);
598✔
203
    }
204

205
    addObserver(
206
        mode: "permanent" | "unscoped" | "scoped",
207
        interpreter: Interpreter,
208
        callable: Callable,
209
        subscriber: RoSGNode,
210
        target: RoSGNode,
211
        fieldName: BrsString
212
    ) {
213
        // Once a field is accessed, it is no longer hidden.
214
        this.hidden = false;
37✔
215

216
        let brsCallback: BrsCallback = {
37✔
217
            interpreter,
218
            environment: interpreter.environment,
219
            hostNode: subscriber,
220
            callable,
221
            eventParams: {
222
                node: target,
223
                fieldName,
224
            },
225
        };
226
        if (mode === "scoped") {
37✔
227
            let maybeCallbacks = this.scopedObservers.get(subscriber) || [];
6✔
228
            this.scopedObservers.set(subscriber, [...maybeCallbacks, brsCallback]);
6✔
229
        } else if (mode === "unscoped") {
31✔
230
            this.unscopedObservers.push(brsCallback);
18✔
231
        } else {
232
            this.permanentObservers.push(brsCallback);
13✔
233
        }
234
    }
235

236
    removeUnscopedObservers() {
237
        this.unscopedObservers.splice(0);
2✔
238
    }
239

240
    removeScopedObservers(hostNode: RoSGNode) {
241
        this.scopedObservers.get(hostNode)?.splice(0);
2!
242
        this.scopedObservers.delete(hostNode);
2✔
243
    }
244

245
    private executeCallbacks(callback: BrsCallback) {
246
        let { interpreter, callable, hostNode, environment, eventParams } = callback;
94✔
247

248
        // Every time a callback happens, a new event is created.
249
        let event = new RoSGNodeEvent(eventParams.node, eventParams.fieldName, this.value);
94✔
250

251
        interpreter.inSubEnv((subInterpreter) => {
94✔
252
            subInterpreter.environment.hostNode = hostNode;
94✔
253
            subInterpreter.environment.setRootM(hostNode.m);
94✔
254

255
            // Check whether the callback is expecting an event parameter.
256
            try {
94✔
257
                if (callable.getFirstSatisfiedSignature([event])) {
94✔
258
                    // m gets lost inside the subinterpreter block in callable.call ?
259
                    callable.call(subInterpreter, event);
81✔
260
                } else {
261
                    callable.call(subInterpreter);
13✔
262
                }
263
            } catch (err) {
264
                if (!(err instanceof BlockEnd)) {
1!
265
                    throw err;
×
266
                }
267
            }
268
            return BrsInvalid.Instance;
94✔
269
        }, environment);
270
    }
271
}
272

273
/* Hierarchy of all node Types. Used to discover is a current node is a subtype of another node */
274
const subtypeHierarchy = new Map<string, string>();
142✔
275

276
/**
277
 *  Checks the node sub type hierarchy to see if the current node is a sub component of the given node type
278
 *
279
 * @param {string} currentNodeType
280
 * @param {string} checkType
281
 * @returns {boolean}
282
 */
283
function isSubtypeCheck(currentNodeType: string, checkType: string): boolean {
284
    checkType = checkType.toLowerCase();
35✔
285
    currentNodeType = currentNodeType.toLowerCase();
35✔
286
    if (currentNodeType === checkType) {
35✔
287
        return true;
12✔
288
    }
289
    let nextNodeType = subtypeHierarchy.get(currentNodeType);
23✔
290
    if (nextNodeType == null) {
23✔
291
        return false;
3✔
292
    }
293
    return isSubtypeCheck(nextNodeType, checkType);
20✔
294
}
295

296
export class RoSGNode extends BrsComponent implements BrsValue, BrsIterable {
142✔
297
    readonly kind = ValueKind.Object;
931✔
298
    private fields = new Map<string, Field>();
931✔
299
    private children: RoSGNode[] = [];
931✔
300
    private parent: RoSGNode | BrsInvalid = BrsInvalid.Instance;
931✔
301

302
    readonly defaultFields: FieldModel[] = [
931✔
303
        { name: "change", type: "roAssociativeArray" },
304
        { name: "focusable", type: "boolean" },
305
        { name: "focusedchild", type: "node", alwaysNotify: true },
306
        { name: "id", type: "string" },
307
    ];
308
    m: RoAssociativeArray = new RoAssociativeArray([]);
931✔
309

310
    constructor(initializedFields: AAMember[], readonly nodeSubtype: string = "Node") {
931✔
311
        super("Node");
931✔
312
        this.setExtendsType();
931✔
313

314
        // All nodes start have some built-in fields when created.
315
        this.registerDefaultFields(this.defaultFields);
931✔
316

317
        // After registering default fields, then register fields instantiated with initial values.
318
        this.registerInitializedFields(initializedFields);
931✔
319

320
        this.registerMethods({
931✔
321
            ifAssociativeArray: [
322
                this.clear,
323
                this.delete,
324
                this.addreplace,
325
                this.count,
326
                this.doesexist,
327
                this.append,
328
                this.keys,
329
                this.items,
330
                this.lookup,
331
                this.lookupCI,
332
            ],
333
            ifSGNodeField: [
334
                this.addfield,
335
                this.addfields,
336
                this.getfield,
337
                this.getfields,
338
                this.hasfield,
339
                this.observefield,
340
                this.unobservefield,
341
                this.observeFieldScoped,
342
                this.unobserveFieldScoped,
343
                this.removefield,
344
                this.setfield,
345
                this.setfields,
346
                this.update,
347
            ],
348
            ifSGNodeChildren: [
349
                this.appendchild,
350
                this.getchildcount,
351
                this.getchildren,
352
                this.removechild,
353
                this.getparent,
354
                this.createchild,
355
                this.replacechild,
356
                this.removechildren,
357
                this.appendchildren,
358
                this.getchild,
359
                this.insertchild,
360
                this.removechildrenindex,
361
                this.removechildindex,
362
                this.reparent,
363
                this.createchildren,
364
                this.replacechildren,
365
                this.insertchildren,
366
            ],
367
            ifSGNodeFocus: [this.hasfocus, this.setfocus, this.isinfocuschain],
368
            ifSGNodeDict: [
369
                this.findnode,
370
                this.issamenode,
371
                this.subtype,
372
                this.callfunc,
373
                this.issubtype,
374
                this.parentsubtype,
375
            ],
376
            ifSGNodeBoundingRect: [this.boundingRect],
377
        });
378
    }
379

380
    toString(parent?: BrsType): string {
381
        let componentName = "roSGNode:" + this.nodeSubtype;
26✔
382

383
        if (parent) {
26✔
384
            return `<Component: ${componentName}>`;
5✔
385
        }
386

387
        return [
21✔
388
            `<Component: ${componentName}> =`,
389
            "{",
390
            ...Array.from(this.fields.entries()).map(
391
                ([key, value]) => `    ${key}: ${value.toString(this)}`
403✔
392
            ),
393
            "}",
394
        ].join("\n");
395
    }
396

397
    equalTo(other: BrsType) {
398
        // SceneGraph nodes are never equal to anything
399
        return BrsBoolean.False;
7✔
400
    }
401

402
    getElements() {
403
        return Array.from(this.fields.keys())
6✔
404
            .sort()
405
            .map((key) => new BrsString(key));
36✔
406
    }
407

408
    getValues() {
409
        return Array.from(this.fields.values())
×
410
            .sort()
411
            .map((field: Field) => field.getValue());
×
412
    }
413

414
    getFields() {
415
        return this.fields;
189✔
416
    }
417

418
    get(index: BrsType) {
419
        if (index.kind !== ValueKind.String) {
925!
420
            throw new Error("RoSGNode indexes must be strings");
×
421
        }
422

423
        // TODO: this works for now, in that a property with the same name as a method essentially
424
        // overwrites the method. The only reason this doesn't work is that getting a method from an
425
        // associative array and _not_ calling it returns `invalid`, but calling it returns the
426
        // function itself. I'm not entirely sure why yet, but it's gotta have something to do with
427
        // how methods are implemented within RBI.
428
        //
429
        // Are they stored separately from elements, like they are here? Or does
430
        // `Interpreter#visitCall` need to check for `invalid` in its callable, then try to find a
431
        // method with the desired name separately? That last bit would work but it's pretty gross.
432
        // That'd allow roArrays to have methods with the methods not accessible via `arr["count"]`.
433
        // Same with RoAssociativeArrays I guess.
434
        let field = this.fields.get(index.value.toLowerCase());
925✔
435
        if (field) {
925✔
436
            return field.getValue();
613✔
437
        }
438
        return this.getMethod(index.value) || BrsInvalid.Instance;
312✔
439
    }
440

441
    set(index: BrsType, value: BrsType, alwaysNotify: boolean = false, kind?: FieldKind) {
591✔
442
        if (index.kind !== ValueKind.String) {
656!
443
            throw new Error("RoSGNode indexes must be strings");
×
444
        }
445

446
        let mapKey = index.value.toLowerCase();
656✔
447
        let fieldType = kind || FieldKind.fromBrsType(value);
656✔
448
        let field = this.fields.get(mapKey);
656✔
449

450
        if (!field) {
656✔
451
            // RBI does not create a new field if the value isn't valid.
452
            if (fieldType) {
121✔
453
                field = new Field(value, fieldType, alwaysNotify);
119✔
454
                this.fields.set(mapKey, field);
119✔
455
            }
456
        } else if (field.canAcceptValue(value)) {
535✔
457
            // Fields are not overwritten if they haven't the same type.
458
            field.setValue(value);
530✔
459
            this.fields.set(mapKey, field);
530✔
460
        }
461

462
        return BrsInvalid.Instance;
656✔
463
    }
464

465
    getParent() {
466
        return this.parent;
7✔
467
    }
468

469
    setParent(parent: RoSGNode) {
470
        this.parent = parent;
336✔
471
    }
472

473
    removeParent() {
474
        this.parent = BrsInvalid.Instance;
51✔
475
    }
476

477
    // recursively search for any child that's focused via DFS
478
    isChildrenFocused(interpreter: Interpreter): boolean {
479
        if (this.children.length === 0) {
142✔
480
            return false;
55✔
481
        }
482

483
        for (let childNode of this.children) {
87✔
484
            if (interpreter.environment.getFocusedNode() === childNode) {
116✔
485
                return true;
22✔
486
            } else if (childNode.isChildrenFocused(interpreter)) {
94✔
487
                return true;
18✔
488
            }
489
        }
490
        return false;
47✔
491
    }
492

493
    /* searches the node tree for a node with the given id */
494
    private findNodeById(node: RoSGNode, id: BrsString): RoSGNode | BrsInvalid {
495
        // test current node in tree
496
        let currentId = node.get(new BrsString("id"));
163✔
497
        if (currentId.toString() === id.toString()) {
163✔
498
            return node;
42✔
499
        }
500

501
        // visit each child
502
        for (let child of node.children) {
121✔
503
            let result = this.findNodeById(child, id);
119✔
504
            if (result instanceof RoSGNode) {
119✔
505
                return result;
63✔
506
            }
507
        }
508

509
        // name was not found anywhere in tree
510
        return BrsInvalid.Instance;
58✔
511
    }
512

513
    private removeChildByReference(child: BrsType): boolean {
514
        if (child instanceof RoSGNode) {
73✔
515
            let spliceIndex = this.children.indexOf(child);
71✔
516
            if (spliceIndex >= 0) {
71✔
517
                child.removeParent();
34✔
518
                this.children.splice(spliceIndex, 1);
34✔
519
            }
520
            return true;
71✔
521
        }
522
        return false;
2✔
523
    }
524

525
    private appendChildToParent(child: BrsType): boolean {
526
        if (child instanceof RoSGNode) {
286✔
527
            if (this.children.includes(child)) {
285✔
528
                return true;
1✔
529
            }
530
            this.children.push(child);
284✔
531
            child.setParent(this);
284✔
532
            return true;
284✔
533
        }
534
        return false;
1✔
535
    }
536

537
    private replaceChildAtIndex(newchild: BrsType, index: Int32): boolean {
538
        let childrenSize = this.children.length;
23✔
539
        let indexValue = index.getValue();
23✔
540
        if (newchild instanceof RoSGNode && indexValue < childrenSize) {
23✔
541
            // If newchild is already a child, remove it first.
542
            this.removeChildByReference(newchild);
17✔
543
            if (indexValue >= 0) {
17✔
544
                // The check is done to see if indexValue is inside the
545
                // new length of this.children (in case newchild was
546
                // removed above)
547
                if (indexValue < this.children.length) {
13✔
548
                    // Remove the parent of the child at indexValue
549
                    this.children[indexValue].removeParent();
12✔
550
                }
551
                newchild.setParent(this);
13✔
552
                this.children.splice(indexValue, 1, newchild);
13✔
553
            }
554
            return true;
17✔
555
        }
556
        return false;
6✔
557
    }
558

559
    private insertChildAtIndex(child: BrsType, index: Int32): boolean {
560
        if (child instanceof RoSGNode) {
18✔
561
            let childrenSize = this.children.length;
18✔
562
            let indexValue = index.getValue() < 0 ? childrenSize : index.getValue();
18✔
563
            // Remove node if it already exists
564
            this.removeChildByReference(child);
18✔
565
            child.setParent(this);
18✔
566
            this.children.splice(indexValue, 0, child);
18✔
567
            return true;
18✔
568
        }
569
        return false;
×
570
    }
571

572
    /* used for isSubtype */
573
    protected setExtendsType() {
574
        let baseClass = this.constructor;
931✔
575
        let currentNodeType: string, parentType: string;
576
        while (baseClass) {
931✔
577
            currentNodeType = baseClass.name.toLowerCase();
1,207✔
578

579
            const parentClass = Object.getPrototypeOf(baseClass);
1,207✔
580

581
            if (parentClass && parentClass !== Object && parentClass.name) {
1,207!
582
                baseClass = parentClass;
1,207✔
583
                parentType = parentClass.name;
1,207✔
584
                if (parentType === "BrsComponent") {
1,207✔
585
                    // Only care about RoSgNode and above
586
                    break;
931✔
587
                }
588
                if (parentType === "RoSGNode") {
276✔
589
                    // RoSGNode is referenced as "Node"
590
                    parentType = "Node";
170✔
591
                }
592
                if (!subtypeHierarchy.has(currentNodeType)) {
276✔
593
                    subtypeHierarchy.set(currentNodeType, parentType);
59✔
594
                }
595
            } else {
596
                break;
×
597
            }
598
        }
599
    }
600

601
    /**
602
     * Calls the function specified on this node.
603
     */
604
    private callfunc = new Callable(
931✔
605
        "callfunc",
606
        ...Callable.variadic({
607
            signature: {
608
                args: [new StdlibArgument("functionname", ValueKind.String)],
609
                returns: ValueKind.Dynamic,
610
            },
611
            impl: (
612
                interpreter: Interpreter,
613
                functionname: BrsString,
614
                ...functionargs: BrsType[]
615
            ) => {
616
                // We need to search the callee's environment for this function rather than the caller's.
617
                let componentDef = interpreter.environment.nodeDefMap.get(
18✔
618
                    this.nodeSubtype.toLowerCase()
619
                );
620

621
                // Only allow public functions (defined in the interface) to be called.
622
                if (componentDef && functionname.value in componentDef.functions) {
18✔
623
                    // Use the mocked component functions instead of the real one, if it's a mocked component.
624
                    if (interpreter.environment.isMockedObject(this.nodeSubtype.toLowerCase())) {
17✔
625
                        let maybeMethod = this.getMethod(functionname.value);
1✔
626
                        return (
1✔
627
                            maybeMethod?.call(interpreter, ...functionargs) || BrsInvalid.Instance
4!
628
                        );
629
                    }
630

631
                    return interpreter.inSubEnv((subInterpreter) => {
16✔
632
                        let functionToCall = subInterpreter.getCallableFunction(functionname.value);
16✔
633
                        if (!functionToCall) {
16✔
634
                            interpreter.stderr.write(
1✔
635
                                `Ignoring attempt to call non-implemented function ${functionname}`
636
                            );
637
                            return BrsInvalid.Instance;
1✔
638
                        }
639

640
                        subInterpreter.environment.setM(this.m);
15✔
641
                        subInterpreter.environment.setRootM(this.m);
15✔
642
                        subInterpreter.environment.hostNode = this;
15✔
643

644
                        try {
15✔
645
                            // Determine whether the function should get arguments or not.
646
                            if (functionToCall.getFirstSatisfiedSignature(functionargs)) {
15✔
647
                                return functionToCall.call(subInterpreter, ...functionargs);
13✔
648
                            } else if (functionToCall.getFirstSatisfiedSignature([])) {
2!
649
                                return functionToCall.call(subInterpreter);
×
650
                            } else {
651
                                return interpreter.addError(
2✔
652
                                    generateArgumentMismatchError(
653
                                        functionToCall,
654
                                        functionargs,
655
                                        subInterpreter.stack[subInterpreter.stack.length - 1]
656
                                    )
657
                                );
658
                            }
659
                        } catch (reason) {
660
                            if (!(reason instanceof Stmt.ReturnValue)) {
12✔
661
                                // re-throw interpreter errors
662
                                throw reason;
2✔
663
                            }
664
                            return reason.value || BrsInvalid.Instance;
10!
665
                        }
666
                    }, componentDef.environment);
667
                }
668

669
                interpreter.stderr.write(
1✔
670
                    `Warning calling function in ${this.nodeSubtype}: no function interface specified for ${functionname}`
671
                );
672
                return BrsInvalid.Instance;
1✔
673
            },
674
        })
675
    );
676

677
    /** Removes all fields from the node */
678
    // ToDo: Built-in fields shouldn't be removed
679
    private clear = new Callable("clear", {
931✔
680
        signature: {
681
            args: [],
682
            returns: ValueKind.Void,
683
        },
684
        impl: (interpreter: Interpreter) => {
685
            this.fields.clear();
2✔
686
            return BrsInvalid.Instance;
2✔
687
        },
688
    });
689

690
    /** Removes a given item from the node */
691
    // ToDo: Built-in fields shouldn't be removed
692
    private delete = new Callable("delete", {
931✔
693
        signature: {
694
            args: [new StdlibArgument("str", ValueKind.String)],
695
            returns: ValueKind.Boolean,
696
        },
697
        impl: (interpreter: Interpreter, str: BrsString) => {
698
            this.fields.delete(str.value.toLowerCase());
5✔
699
            return BrsBoolean.True; //RBI always returns true
5✔
700
        },
701
    });
702

703
    /** Given a key and value, adds an item to the node if it doesn't exist
704
     * Or replaces the value of a key that already exists in the node
705
     */
706
    private addreplace = new Callable("addreplace", {
931✔
707
        signature: {
708
            args: [
709
                new StdlibArgument("key", ValueKind.String),
710
                new StdlibArgument("value", ValueKind.Dynamic),
711
            ],
712
            returns: ValueKind.Void,
713
        },
714
        impl: (interpreter: Interpreter, key: BrsString, value: BrsType) => {
715
            this.set(key, value);
3✔
716
            return BrsInvalid.Instance;
3✔
717
        },
718
    });
719

720
    /** Returns the number of items in the node */
721
    protected count = new Callable("count", {
931✔
722
        signature: {
723
            args: [],
724
            returns: ValueKind.Int32,
725
        },
726
        impl: (interpreter: Interpreter) => {
727
            return new Int32(this.fields.size);
5✔
728
        },
729
    });
730

731
    /** Returns a boolean indicating whether or not a given key exists in the node */
732
    private doesexist = new Callable("doesexist", {
931✔
733
        signature: {
734
            args: [new StdlibArgument("str", ValueKind.String)],
735
            returns: ValueKind.Boolean,
736
        },
737
        impl: (interpreter: Interpreter, str: BrsString) => {
738
            return this.get(str) !== BrsInvalid.Instance ? BrsBoolean.True : BrsBoolean.False;
5✔
739
        },
740
    });
741

742
    /** Appends a new node to another. If two keys are the same, the value of the original AA is replaced with the new one. */
743
    private append = new Callable("append", {
931✔
744
        signature: {
745
            args: [new StdlibArgument("obj", ValueKind.Object)],
746
            returns: ValueKind.Void,
747
        },
748
        impl: (interpreter: Interpreter, obj: BrsType) => {
749
            if (obj instanceof RoAssociativeArray) {
2✔
750
                obj.elements.forEach((value, key) => {
1✔
751
                    let fieldType = FieldKind.fromBrsType(value);
1✔
752

753
                    // if the field doesn't have a valid value, RBI doesn't add it.
754
                    if (fieldType) {
1✔
755
                        this.fields.set(key, new Field(value, fieldType, false));
1✔
756
                    }
757
                });
758
            } else if (obj instanceof RoSGNode) {
1✔
759
                obj.getFields().forEach((value, key) => {
1✔
760
                    this.fields.set(key, value);
9✔
761
                });
762
            }
763

764
            return BrsInvalid.Instance;
2✔
765
        },
766
    });
767

768
    /** Returns an array of keys from the node in lexicographical order */
769
    protected keys = new Callable("keys", {
931✔
770
        signature: {
771
            args: [],
772
            returns: ValueKind.Object,
773
        },
774
        impl: (interpreter: Interpreter) => {
775
            return new RoArray(this.getElements());
3✔
776
        },
777
    });
778

779
    /** Returns an array of key/value pairs in lexicographical order of key. */
780
    protected items = new Callable("items", {
931✔
781
        signature: {
782
            args: [],
783
            returns: ValueKind.Object,
784
        },
785
        impl: (interpreter: Interpreter) => {
786
            return new RoArray(
3✔
787
                this.getElements().map((key: BrsString) => {
788
                    return new RoAssociativeArray([
18✔
789
                        {
790
                            name: new BrsString("key"),
791
                            value: key,
792
                        },
793
                        {
794
                            name: new BrsString("value"),
795
                            value: this.get(key),
796
                        },
797
                    ]);
798
                })
799
            );
800
        },
801
    });
802

803
    /** Given a key, returns the value associated with that key. This method is case insensitive. */
804
    private lookup = new Callable("lookup", {
931✔
805
        signature: {
806
            args: [new StdlibArgument("key", ValueKind.String)],
807
            returns: ValueKind.Dynamic,
808
        },
809
        impl: (interpreter: Interpreter, key: BrsString) => {
810
            let lKey = key.value.toLowerCase();
4✔
811
            return this.get(new BrsString(lKey));
4✔
812
        },
813
    });
814

815
    /** Given a key, returns the value associated with that key. This method is case insensitive. */
816
    private lookupCI = new Callable("lookupCI", this.lookup.signatures[0]);
931✔
817

818
    /** Adds a new field to the node, if the field already exists it doesn't change the current value. */
819
    private addfield = new Callable("addfield", {
931✔
820
        signature: {
821
            args: [
822
                new StdlibArgument("fieldname", ValueKind.String),
823
                new StdlibArgument("type", ValueKind.String),
824
                new StdlibArgument("alwaysnotify", ValueKind.Boolean),
825
            ],
826
            returns: ValueKind.Boolean,
827
        },
828
        impl: (
829
            interpreter: Interpreter,
830
            fieldname: BrsString,
831
            type: BrsString,
832
            alwaysnotify: BrsBoolean
833
        ) => {
834
            let defaultValue = getBrsValueFromFieldType(type.value);
66✔
835
            let fieldKind = FieldKind.fromString(type.value);
66✔
836

837
            if (defaultValue !== Uninitialized.Instance && !this.fields.has(fieldname.value)) {
66✔
838
                this.set(fieldname, defaultValue, alwaysnotify.toBoolean(), fieldKind);
65✔
839
            }
840

841
            return BrsBoolean.True;
66✔
842
        },
843
    });
844

845
    /** Adds one or more fields defined as an associative aray of key values. */
846
    private addfields = new Callable("addfields", {
931✔
847
        signature: {
848
            args: [new StdlibArgument("fields", ValueKind.Object)],
849
            returns: ValueKind.Boolean,
850
        },
851
        impl: (interpreter: Interpreter, fields: RoAssociativeArray) => {
852
            if (!(fields instanceof RoAssociativeArray)) {
12✔
853
                return BrsBoolean.False;
1✔
854
            }
855

856
            fields.getValue().forEach((value, key) => {
11✔
857
                let fieldName = new BrsString(key);
26✔
858
                if (!this.fields.has(key)) {
26✔
859
                    this.set(fieldName, value);
24✔
860
                }
861
            });
862

863
            return BrsBoolean.True;
11✔
864
        },
865
    });
866

867
    /** Returns the value of the field passed as argument, if the field doesn't exist it returns invalid. */
868
    private getfield = new Callable("getfield", {
931✔
869
        signature: {
870
            args: [new StdlibArgument("fieldname", ValueKind.String)],
871
            returns: ValueKind.Dynamic,
872
        },
873
        impl: (interpreter: Interpreter, fieldname: BrsString) => {
874
            return this.get(fieldname);
25✔
875
        },
876
    });
877

878
    /** Returns the names and values of all the fields in the node. */
879
    private getfields = new Callable("getfields", {
931✔
880
        signature: {
881
            args: [],
882
            returns: ValueKind.Object,
883
        },
884
        impl: (interpreter: Interpreter) => {
885
            let packagedFields: AAMember[] = [];
2✔
886

887
            this.fields.forEach((field, name) => {
2✔
888
                if (field.isHidden()) {
6!
889
                    return;
×
890
                }
891

892
                packagedFields.push({
6✔
893
                    name: new BrsString(name),
894
                    value: field.getValue(),
895
                });
896
            });
897

898
            return new RoAssociativeArray(packagedFields);
2✔
899
        },
900
    });
901

902
    /** Returns true if the field exists */
903
    protected hasfield = new Callable("hasfield", {
931✔
904
        signature: {
905
            args: [new StdlibArgument("fieldname", ValueKind.String)],
906
            returns: ValueKind.Boolean,
907
        },
908
        impl: (interpreter: Interpreter, fieldname: BrsString) => {
909
            return this.fields.has(fieldname.value.toLowerCase())
7✔
910
                ? BrsBoolean.True
911
                : BrsBoolean.False;
912
        },
913
    });
914

915
    /** Registers a callback to be executed when the value of the field changes */
916
    private observefield = new Callable("observefield", {
931✔
917
        signature: {
918
            args: [
919
                new StdlibArgument("fieldname", ValueKind.String),
920
                new StdlibArgument("functionname", ValueKind.String),
921
            ],
922
            returns: ValueKind.Boolean,
923
        },
924
        impl: (interpreter: Interpreter, fieldname: BrsString, functionname: BrsString) => {
925
            let field = this.fields.get(fieldname.value.toLowerCase());
19✔
926
            if (field instanceof Field) {
19✔
927
                let callableFunction = interpreter.getCallableFunction(functionname.value);
18✔
928
                let subscriber = interpreter.environment.hostNode;
18✔
929
                if (!subscriber) {
18!
NEW
930
                    let location = interpreter.formatLocation();
×
931
                    interpreter.stderr.write(
×
932
                        `BRIGHTSCRIPT: ERROR: roSGNode.ObserveField: no active host node: ${location}\n`
933
                    );
934
                    return BrsBoolean.False;
×
935
                }
936

937
                if (callableFunction && subscriber) {
18!
938
                    field.addObserver(
18✔
939
                        "unscoped",
940
                        interpreter,
941
                        callableFunction,
942
                        subscriber,
943
                        this,
944
                        fieldname
945
                    );
946
                } else {
947
                    return BrsBoolean.False;
×
948
                }
949
            }
950
            return BrsBoolean.True;
19✔
951
        },
952
    });
953

954
    /**
955
     * Removes all observers of a given field, regardless of whether or not the host node is the subscriber.
956
     */
957
    private unobservefield = new Callable("unobservefield", {
931✔
958
        signature: {
959
            args: [new StdlibArgument("fieldname", ValueKind.String)],
960
            returns: ValueKind.Boolean,
961
        },
962
        impl: (interpreter: Interpreter, fieldname: BrsString, functionname: BrsString) => {
963
            if (!interpreter.environment.hostNode) {
2!
NEW
964
                let location = interpreter.formatLocation();
×
965
                interpreter.stderr.write(
×
966
                    `BRIGHTSCRIPT: ERROR: roSGNode.unObserveField: no active host node: ${location}\n`
967
                );
968
                return BrsBoolean.False;
×
969
            }
970

971
            let field = this.fields.get(fieldname.value.toLowerCase());
2✔
972
            if (field instanceof Field) {
2✔
973
                field.removeUnscopedObservers();
2✔
974
            }
975
            // returns true, even if the field doesn't exist
976
            return BrsBoolean.True;
2✔
977
        },
978
    });
979

980
    private observeFieldScoped = new Callable("observeFieldSCoped", {
931✔
981
        signature: {
982
            args: [
983
                new StdlibArgument("fieldname", ValueKind.String),
984
                new StdlibArgument("functionname", ValueKind.String),
985
            ],
986
            returns: ValueKind.Boolean,
987
        },
988
        impl: (interpreter: Interpreter, fieldname: BrsString, functionname: BrsString) => {
989
            let field = this.fields.get(fieldname.value.toLowerCase());
6✔
990
            if (field instanceof Field) {
6✔
991
                let callableFunction = interpreter.getCallableFunction(functionname.value);
6✔
992
                let subscriber = interpreter.environment.hostNode;
6✔
993
                if (!subscriber) {
6!
NEW
994
                    let location = interpreter.formatLocation();
×
995
                    interpreter.stderr.write(
×
996
                        `BRIGHTSCRIPT: ERROR: roSGNode.ObserveField: no active host node: ${location}\n`
997
                    );
998
                    return BrsBoolean.False;
×
999
                }
1000

1001
                if (callableFunction && subscriber) {
6!
1002
                    field.addObserver(
6✔
1003
                        "scoped",
1004
                        interpreter,
1005
                        callableFunction,
1006
                        subscriber,
1007
                        this,
1008
                        fieldname
1009
                    );
1010
                } else {
1011
                    return BrsBoolean.False;
×
1012
                }
1013
            }
1014
            return BrsBoolean.True;
6✔
1015
        },
1016
    });
1017

1018
    private unobserveFieldScoped = new Callable("unobserveFieldScoped", {
931✔
1019
        signature: {
1020
            args: [new StdlibArgument("fieldname", ValueKind.String)],
1021
            returns: ValueKind.Boolean,
1022
        },
1023
        impl: (interpreter: Interpreter, fieldname: BrsString, functionname: BrsString) => {
1024
            if (!interpreter.environment.hostNode) {
2!
NEW
1025
                let location = interpreter.formatLocation();
×
1026
                interpreter.stderr.write(
×
1027
                    `BRIGHTSCRIPT: ERROR: roSGNode.unObserveField: no active host node: ${location}\n`
1028
                );
1029
                return BrsBoolean.False;
×
1030
            }
1031

1032
            let field = this.fields.get(fieldname.value.toLowerCase());
2✔
1033
            if (field instanceof Field) {
2✔
1034
                field.removeScopedObservers(interpreter.environment.hostNode);
2✔
1035
            }
1036
            // returns true, even if the field doesn't exist
1037
            return BrsBoolean.True;
2✔
1038
        },
1039
    });
1040

1041
    /** Removes the given field from the node */
1042
    /** TODO: node built-in fields shouldn't be removable (i.e. id, change, focusable,) */
1043
    private removefield = new Callable("removefield", {
931✔
1044
        signature: {
1045
            args: [new StdlibArgument("fieldname", ValueKind.String)],
1046
            returns: ValueKind.Boolean,
1047
        },
1048
        impl: (interpreter: Interpreter, fieldname: BrsString) => {
1049
            this.fields.delete(fieldname.value.toLowerCase());
5✔
1050
            return BrsBoolean.True; //RBI always returns true
5✔
1051
        },
1052
    });
1053

1054
    /** Updates the value of an existing field only if the types match. */
1055
    private setfield = new Callable("setfield", {
931✔
1056
        signature: {
1057
            args: [
1058
                new StdlibArgument("fieldname", ValueKind.String),
1059
                new StdlibArgument("value", ValueKind.Dynamic),
1060
            ],
1061
            returns: ValueKind.Boolean,
1062
        },
1063
        impl: (interpreter: Interpreter, fieldname: BrsString, value: BrsType) => {
1064
            let field = this.fields.get(fieldname.value.toLowerCase());
342✔
1065
            if (!field) {
342✔
1066
                return BrsBoolean.False;
1✔
1067
            }
1068

1069
            if (!field.canAcceptValue(value)) {
341✔
1070
                return BrsBoolean.False;
1✔
1071
            }
1072

1073
            this.set(fieldname, value);
340✔
1074
            return BrsBoolean.True;
340✔
1075
        },
1076
    });
1077

1078
    /** Updates the value of multiple existing field only if the types match. */
1079
    private setfields = new Callable("setfields", {
931✔
1080
        signature: {
1081
            args: [new StdlibArgument("fields", ValueKind.Object)],
1082
            returns: ValueKind.Boolean,
1083
        },
1084
        impl: (interpreter: Interpreter, fields: RoAssociativeArray) => {
1085
            if (!(fields instanceof RoAssociativeArray)) {
3!
1086
                return BrsBoolean.False;
×
1087
            }
1088

1089
            fields.getValue().forEach((value, key) => {
3✔
1090
                let fieldName = new BrsString(key);
6✔
1091
                if (this.fields.has(key)) {
6✔
1092
                    this.set(fieldName, value);
6✔
1093
                }
1094
            });
1095

1096
            return BrsBoolean.True;
3✔
1097
        },
1098
    });
1099

1100
    /* Updates the value of multiple existing field only if the types match.
1101
    In contrast to setFields method, update always return Uninitialized */
1102
    private update = new Callable("update", {
931✔
1103
        signature: {
1104
            args: [
1105
                new StdlibArgument("aa", ValueKind.Object),
1106
                new StdlibArgument("createFields", ValueKind.Boolean, BrsBoolean.False),
1107
            ],
1108
            returns: ValueKind.Uninitialized,
1109
        },
1110
        impl: (interpreter: Interpreter, aa: RoAssociativeArray, createFields: BrsBoolean) => {
1111
            if (!(aa instanceof RoAssociativeArray)) {
16!
1112
                return Uninitialized.Instance;
×
1113
            }
1114

1115
            aa.getValue().forEach((value, key) => {
16✔
1116
                let fieldName = new BrsString(key);
25✔
1117
                if (this.fields.has(key.toLowerCase()) || createFields.toBoolean()) {
25✔
1118
                    this.set(fieldName, value);
20✔
1119
                }
1120
            });
1121

1122
            return Uninitialized.Instance;
16✔
1123
        },
1124
    });
1125

1126
    /* Return the current number of children in the subject node list of children.
1127
    This is always a non-negative number. */
1128
    private getchildcount = new Callable("getchildcount", {
931✔
1129
        signature: {
1130
            args: [],
1131
            returns: ValueKind.Int32,
1132
        },
1133
        impl: (interpreter: Interpreter) => {
1134
            return new Int32(this.children.length);
44✔
1135
        },
1136
    });
1137

1138
    /* Adds a child node to the end of the subject node list of children so that it is
1139
    traversed last (of those children) during render. */
1140
    private appendchild = new Callable("appendchild", {
931✔
1141
        signature: {
1142
            args: [new StdlibArgument("child", ValueKind.Dynamic)],
1143
            returns: ValueKind.Boolean,
1144
        },
1145
        impl: (interpreter: Interpreter, child: BrsType) => {
1146
            return BrsBoolean.from(this.appendChildToParent(child));
275✔
1147
        },
1148
    });
1149

1150
    /* Retrieves the number of child nodes specified by num_children from the subject
1151
    node, starting at the position specified by index. Returns an array of the child nodes
1152
    retrieved. If num_children is -1, return all the children. */
1153
    private getchildren = new Callable("getchildren", {
931✔
1154
        signature: {
1155
            args: [
1156
                new StdlibArgument("num_children", ValueKind.Int32),
1157
                new StdlibArgument("index", ValueKind.Int32),
1158
            ],
1159
            returns: ValueKind.Object,
1160
        },
1161
        impl: (interpreter: Interpreter, num_children: Int32, index: Int32) => {
1162
            let numChildrenValue = num_children.getValue();
45✔
1163
            let indexValue = index.getValue();
45✔
1164
            let childrenSize = this.children.length;
45✔
1165
            if (numChildrenValue <= -1 && indexValue === 0) {
45✔
1166
                //short hand to return all children
1167
                return new RoArray(this.children.slice());
40✔
1168
            } else if (numChildrenValue <= 0 || indexValue < 0 || indexValue >= childrenSize) {
5✔
1169
                //these never return any children
1170
                return new RoArray([]);
2✔
1171
            } else {
1172
                //only valid cases
1173
                return new RoArray(this.children.slice(indexValue, indexValue + numChildrenValue));
3✔
1174
            }
1175

1176
            return new RoArray([]);
×
1177
        },
1178
    });
1179

1180
    /* Finds a child node in the subject node list of children, and if found,
1181
    remove it from the list of children. The match is made on the basis of actual
1182
    object identity, that is, the value of the pointer to the child node.
1183
    return false if trying to remove anything that's not a node */
1184
    private removechild = new Callable("removechild", {
931✔
1185
        signature: {
1186
            args: [new StdlibArgument("child", ValueKind.Dynamic)],
1187
            returns: ValueKind.Boolean,
1188
        },
1189
        impl: (interpreter: Interpreter, child: BrsType) => {
1190
            return BrsBoolean.from(this.removeChildByReference(child));
9✔
1191
        },
1192
    });
1193
    /* If the subject node has been added to a parent node list of children,
1194
    return the parent node, otherwise return invalid.*/
1195
    private getparent = new Callable("getparent", {
931✔
1196
        signature: {
1197
            args: [],
1198
            returns: ValueKind.Dynamic,
1199
        },
1200
        impl: (interpreter: Interpreter) => {
1201
            return this.parent;
8✔
1202
        },
1203
    });
1204

1205
    /* Creates a child node of type nodeType, and adds the new node to the end of the
1206
    subject node list of children */
1207
    private createchild = new Callable("createchild", {
931✔
1208
        signature: {
1209
            args: [new StdlibArgument("nodetype", ValueKind.String)],
1210
            returns: ValueKind.Object,
1211
        },
1212
        impl: (interpreter: Interpreter, nodetype: BrsString) => {
1213
            // currently we can't create a custom subclass object of roSGNode,
1214
            // so we'll always create generic RoSGNode object as child
1215
            let child = createNodeByType(interpreter, nodetype);
17✔
1216
            if (child instanceof RoSGNode) {
17✔
1217
                this.children.push(child);
17✔
1218
                child.setParent(this);
17✔
1219
            }
1220
            return child;
17✔
1221
        },
1222
    });
1223

1224
    /**
1225
     * If the subject node has a child node in the index position, replace that child
1226
     * node with the newChild node in the subject node list of children, otherwise do nothing.
1227
     */
1228

1229
    private replacechild = new Callable("replacechild", {
931✔
1230
        signature: {
1231
            args: [
1232
                new StdlibArgument("newchild", ValueKind.Dynamic),
1233
                new StdlibArgument("index", ValueKind.Int32),
1234
            ],
1235
            returns: ValueKind.Boolean,
1236
        },
1237
        impl: (interpreter: Interpreter, newchild: BrsType, index: Int32) => {
1238
            return BrsBoolean.from(this.replaceChildAtIndex(newchild, index));
9✔
1239
        },
1240
    });
1241

1242
    /**
1243
     * Removes the child nodes specified by child_nodes from the subject node. Returns
1244
     * true if the child nodes were successfully removed.
1245
     */
1246
    private removechildren = new Callable("removechildren", {
931✔
1247
        signature: {
1248
            args: [new StdlibArgument("child_nodes", ValueKind.Dynamic)],
1249
            returns: ValueKind.Boolean,
1250
        },
1251
        impl: (interpreter: Interpreter, child_nodes: BrsType) => {
1252
            if (child_nodes instanceof RoArray) {
6✔
1253
                let childNodesElements = child_nodes.getElements();
6✔
1254
                if (childNodesElements.length !== 0) {
6✔
1255
                    childNodesElements.forEach((childNode) => {
5✔
1256
                        this.removeChildByReference(childNode);
11✔
1257
                    });
1258
                    return BrsBoolean.True;
5✔
1259
                }
1260
            }
1261
            return BrsBoolean.False;
1✔
1262
        },
1263
    });
1264

1265
    /**
1266
     * Removes the number of child nodes specified by num_children from the subject node
1267
     * starting at the position specified by index.
1268
     */
1269
    private removechildrenindex = new Callable("removechildrenindex", {
931✔
1270
        signature: {
1271
            args: [
1272
                new StdlibArgument("num_children", ValueKind.Int32),
1273
                new StdlibArgument("index", ValueKind.Int32),
1274
            ],
1275
            returns: ValueKind.Boolean,
1276
        },
1277
        impl: (interpreter: Interpreter, num_children: Int32, index: Int32) => {
1278
            let numChildrenValue = num_children.getValue();
5✔
1279
            let indexValue = index.getValue();
5✔
1280

1281
            if (numChildrenValue > 0) {
5✔
1282
                let removedChildren = this.children.splice(indexValue, numChildrenValue);
4✔
1283
                removedChildren.forEach((node) => {
4✔
1284
                    node.removeParent();
5✔
1285
                });
1286
                return BrsBoolean.True;
4✔
1287
            }
1288
            return BrsBoolean.False;
1✔
1289
        },
1290
    });
1291

1292
    /**
1293
     * If the subject node has a child node at the index position, return it, otherwise
1294
     * return invalid.
1295
     */
1296
    private getchild = new Callable("getchild", {
931✔
1297
        signature: {
1298
            args: [new StdlibArgument("index", ValueKind.Int32)],
1299
            returns: ValueKind.Dynamic,
1300
        },
1301
        impl: (interpreter: Interpreter, index: Int32) => {
1302
            let indexValue = index.getValue();
5✔
1303
            let childrenSize = this.children.length;
5✔
1304

1305
            if (indexValue >= 0 && indexValue < childrenSize) {
5✔
1306
                return this.children[indexValue];
3✔
1307
            }
1308
            return BrsInvalid.Instance;
2✔
1309
        },
1310
    });
1311

1312
    /**
1313
     * Appends the nodes specified by child_nodes to the subject node.
1314
     */
1315
    private appendchildren = new Callable("appendchildren", {
931✔
1316
        signature: {
1317
            args: [new StdlibArgument("child_nodes", ValueKind.Dynamic)],
1318
            returns: ValueKind.Boolean,
1319
        },
1320
        impl: (interpreter: Interpreter, child_nodes: BrsType) => {
1321
            if (child_nodes instanceof RoArray) {
5✔
1322
                let childNodesElements = child_nodes.getElements();
5✔
1323
                if (childNodesElements.length !== 0) {
5✔
1324
                    childNodesElements.forEach((childNode) => {
4✔
1325
                        if (childNode instanceof RoSGNode) {
8✔
1326
                            // Remove if it exists to reappend
1327
                            this.removeChildByReference(childNode);
8✔
1328
                            this.appendChildToParent(childNode);
8✔
1329
                        }
1330
                    });
1331
                    return BrsBoolean.True;
4✔
1332
                }
1333
            }
1334
            return BrsBoolean.False;
1✔
1335
        },
1336
    });
1337

1338
    /** Creates the number of children specified by num_children for the subject node,
1339
     *  of the type or extended type specified by subtype.
1340
     */
1341
    private createchildren = new Callable("createchildren", {
931✔
1342
        signature: {
1343
            args: [
1344
                new StdlibArgument("num_children", ValueKind.Int32),
1345
                new StdlibArgument("subtype", ValueKind.String),
1346
            ],
1347
            returns: ValueKind.Dynamic,
1348
        },
1349
        impl: (interpreter: Interpreter, num_children: Int32, subtype: BrsString) => {
1350
            let numChildrenValue = num_children.getValue();
3✔
1351
            let addedChildren: RoSGNode[] = [];
3✔
1352
            for (let i = 0; i < numChildrenValue; i++) {
3✔
1353
                let child = createNodeByType(interpreter, subtype);
4✔
1354
                if (child instanceof RoSGNode) {
4✔
1355
                    this.children.push(child);
4✔
1356
                    addedChildren.push(child);
4✔
1357
                    child.setParent(this);
4✔
1358
                }
1359
            }
1360
            return new RoArray(addedChildren);
3✔
1361
        },
1362
    });
1363

1364
    /** Replaces the child nodes in the subject node, starting at the position specified
1365
     *  by index, with new child nodes specified by child_nodes.
1366
     */
1367
    private replacechildren = new Callable("replacechildren", {
931✔
1368
        signature: {
1369
            args: [
1370
                new StdlibArgument("child_nodes", ValueKind.Dynamic),
1371
                new StdlibArgument("index", ValueKind.Int32),
1372
            ],
1373
            returns: ValueKind.Boolean,
1374
        },
1375
        impl: (interpreter: Interpreter, child_nodes: BrsType, index: Int32) => {
1376
            if (child_nodes instanceof RoArray) {
8✔
1377
                let indexValue = index.getValue();
8✔
1378
                let childNodesElements = child_nodes.getElements();
8✔
1379
                if (childNodesElements.length !== 0) {
8✔
1380
                    childNodesElements.forEach((childNode) => {
7✔
1381
                        if (!this.replaceChildAtIndex(childNode, new Int32(indexValue))) {
14✔
1382
                            this.removeChildByReference(childNode);
5✔
1383
                        }
1384
                        indexValue += 1;
14✔
1385
                    });
1386
                    return BrsBoolean.True;
7✔
1387
                }
1388
            }
1389
            return BrsBoolean.False;
1✔
1390
        },
1391
    });
1392

1393
    /**
1394
     * Inserts the child nodes specified by child_nodes to the subject node starting
1395
     * at the position specified by index.
1396
     */
1397
    private insertchildren = new Callable("insertchildren", {
931✔
1398
        signature: {
1399
            args: [
1400
                new StdlibArgument("child_nodes", ValueKind.Dynamic),
1401
                new StdlibArgument("index", ValueKind.Int32),
1402
            ],
1403
            returns: ValueKind.Boolean,
1404
        },
1405
        impl: (interpreter: Interpreter, child_nodes: BrsType, index: Int32) => {
1406
            if (child_nodes instanceof RoArray) {
7✔
1407
                let indexValue = index.getValue();
7✔
1408
                let childNodesElements = child_nodes.getElements();
7✔
1409
                if (childNodesElements.length !== 0) {
7✔
1410
                    childNodesElements.forEach((childNode) => {
6✔
1411
                        this.insertChildAtIndex(childNode, new Int32(indexValue));
12✔
1412
                        indexValue += 1;
12✔
1413
                    });
1414
                    return BrsBoolean.True;
6✔
1415
                }
1416
            }
1417
            return BrsBoolean.False;
1✔
1418
        },
1419
    });
1420

1421
    /**
1422
     * Inserts a previously-created child node at the position index in the subject
1423
     * node list of children, so that this is the position that the new child node
1424
     * is traversed during render.
1425
     */
1426
    private insertchild = new Callable("insertchild", {
931✔
1427
        signature: {
1428
            args: [
1429
                new StdlibArgument("child", ValueKind.Dynamic),
1430
                new StdlibArgument("index", ValueKind.Int32),
1431
            ],
1432
            returns: ValueKind.Boolean,
1433
        },
1434
        impl: (interpreter: Interpreter, child: BrsType, index: Int32) => {
1435
            return BrsBoolean.from(this.insertChildAtIndex(child, index));
6✔
1436
        },
1437
    });
1438

1439
    /**
1440
     * If the subject node has a child node in the index position, remove that child
1441
     * node from the subject node list of children.
1442
     */
1443
    private removechildindex = new Callable("removechildindex", {
931✔
1444
        signature: {
1445
            args: [new StdlibArgument("index", ValueKind.Int32)],
1446
            returns: ValueKind.Boolean,
1447
        },
1448
        impl: (interpreter: Interpreter, index: Int32) => {
1449
            let indexValue = index.getValue();
7✔
1450
            let childrenSize = this.children.length;
7✔
1451

1452
            if (indexValue < childrenSize) {
7✔
1453
                if (indexValue >= 0) {
5✔
1454
                    this.removeChildByReference(this.children[indexValue]);
3✔
1455
                }
1456
                return BrsBoolean.True;
5✔
1457
            }
1458
            return BrsBoolean.False;
2✔
1459
        },
1460
    });
1461

1462
    /**
1463
     * Moves the subject node to another node.
1464
     * If adjustTransform is true, the subject node transformation factor fields (translation/rotation/scale)
1465
     * are adjusted so that the node has the same transformation factors relative to the screen as it previously did.
1466
     * If adjustTransform is false, the subject node is simply parented to the new node without adjusting its
1467
     * transformation factor fields, in which case, the reparenting operation could cause the node to jump to a
1468
     * new position on the screen.
1469
     */
1470
    private reparent = new Callable("reparent", {
931✔
1471
        signature: {
1472
            args: [
1473
                new StdlibArgument("newParent", ValueKind.Dynamic),
1474
                new StdlibArgument("adjustTransform", ValueKind.Boolean),
1475
            ],
1476
            returns: ValueKind.Boolean,
1477
        },
1478
        impl: (interpreter: Interpreter, newParent: BrsType, adjustTransform: BrsBoolean) => {
1479
            if (newParent instanceof RoSGNode && newParent !== this) {
4✔
1480
                // TODO: adjustTransform has to be implemented probably by traversing the
1481
                // entire parent tree to get to the top, calculate the absolute transform
1482
                // parameters and then use that to adjust the new transform properties.
1483
                // Until that is implemented, the parameter does nothing.
1484

1485
                // Remove parents child reference
1486
                if (this.parent instanceof RoSGNode) {
3✔
1487
                    this.parent.removeChildByReference(this);
2✔
1488
                }
1489
                newParent.appendChildToParent(this);
3✔
1490
                return BrsBoolean.True;
3✔
1491
            }
1492
            return BrsBoolean.False;
1✔
1493
        },
1494
    });
1495

1496
    /* Returns true if the subject node has the remote control focus, and false otherwise */
1497
    private hasfocus = new Callable("hasfocus", {
931✔
1498
        signature: {
1499
            args: [],
1500
            returns: ValueKind.Boolean,
1501
        },
1502
        impl: (interpreter: Interpreter) => {
1503
            return BrsBoolean.from(interpreter.environment.getFocusedNode() === this);
13✔
1504
        },
1505
    });
1506

1507
    private boundingRect = new Callable("boundingRect", {
931✔
1508
        signature: {
1509
            args: [],
1510
            returns: ValueKind.Dynamic,
1511
        },
1512
        impl: (interpreter: Interpreter) => {
1513
            const zeroValue = new Int32(0);
2✔
1514
            return new RoAssociativeArray([
2✔
1515
                { name: new BrsString("x"), value: zeroValue },
1516
                { name: new BrsString("y"), value: zeroValue },
1517
                { name: new BrsString("height"), value: zeroValue },
1518
                { name: new BrsString("width"), value: zeroValue },
1519
            ]);
1520
        },
1521
    });
1522

1523
    /**
1524
     * Starting with a leaf node, traverses upward through the parents until it reaches
1525
     * a node without a parent (root node).
1526
     * @param {RoSGNode} node The leaf node to create the tree with
1527
     * @returns RoSGNode[] The parent chain starting with root-most parent
1528
     */
1529
    private createPath(node: RoSGNode): RoSGNode[] {
1530
        let path: RoSGNode[] = [node];
34✔
1531

1532
        while (node.parent instanceof RoSGNode) {
34✔
1533
            path.push(node.parent);
60✔
1534
            node = node.parent;
60✔
1535
        }
1536

1537
        return path.reverse();
34✔
1538
    }
1539

1540
    /**
1541
     *  If on is set to true, sets the current remote control focus to the subject node,
1542
     *  also automatically removing it from the node on which it was previously set.
1543
     *  If on is set to false, removes focus from the subject node if it had it.
1544
     *
1545
     *  It also runs through all of the ancestors of the node that was focused prior to this call,
1546
     *  and the newly focused node, and sets the `focusedChild` field of each to reflect the new state.
1547
     */
1548
    private setfocus = new Callable("setfocus", {
931✔
1549
        signature: {
1550
            args: [new StdlibArgument("on", ValueKind.Boolean)],
1551
            returns: ValueKind.Boolean,
1552
        },
1553
        impl: (interpreter: Interpreter, on: BrsBoolean) => {
1554
            let focusedChildString = new BrsString("focusedchild");
26✔
1555
            let currFocusedNode = interpreter.environment.getFocusedNode();
26✔
1556

1557
            if (on.toBoolean()) {
26✔
1558
                interpreter.environment.setFocusedNode(this);
19✔
1559

1560
                // Get the focus chain, with lowest ancestor first.
1561
                let newFocusChain = this.createPath(this);
19✔
1562

1563
                // If there's already a focused node somewhere, we need to remove focus
1564
                // from it and its ancestors.
1565
                if (currFocusedNode instanceof RoSGNode) {
19✔
1566
                    // Get the focus chain, with root-most ancestor first.
1567
                    let currFocusChain = this.createPath(currFocusedNode);
8✔
1568

1569
                    // Find the lowest common ancestor (LCA) between the newly focused node
1570
                    // and the current focused node.
1571
                    let lcaIndex = 0;
8✔
1572
                    while (lcaIndex < newFocusChain.length && lcaIndex < currFocusChain.length) {
8✔
1573
                        if (currFocusChain[lcaIndex] !== newFocusChain[lcaIndex]) break;
14✔
1574
                        lcaIndex++;
7✔
1575
                    }
1576

1577
                    // Unset all of the not-common ancestors of the current focused node.
1578
                    for (let i = lcaIndex; i < currFocusChain.length; i++) {
8✔
1579
                        currFocusChain[i].set(focusedChildString, BrsInvalid.Instance);
15✔
1580
                    }
1581
                }
1582

1583
                // Set the focusedChild for each ancestor to the next node in the chain,
1584
                // which is the current node's child.
1585
                for (let i = 0; i < newFocusChain.length - 1; i++) {
19✔
1586
                    newFocusChain[i].set(focusedChildString, newFocusChain[i + 1]);
31✔
1587
                }
1588

1589
                // Finally, set the focusedChild of the newly focused node to itself (to mimic RBI behavior).
1590
                this.set(focusedChildString, this);
19✔
1591
            } else {
1592
                interpreter.environment.setFocusedNode(BrsInvalid.Instance);
7✔
1593

1594
                // If we're unsetting focus on ourself, we need to unset it on all ancestors as well.
1595
                if (currFocusedNode === this) {
7!
1596
                    // Get the focus chain, with root-most ancestor first.
1597
                    let currFocusChain = this.createPath(currFocusedNode);
7✔
1598
                    currFocusChain.forEach((node) => {
7✔
1599
                        node.set(focusedChildString, BrsInvalid.Instance);
22✔
1600
                    });
1601
                } else {
1602
                    // If the node doesn't have focus already, and it's not gaining focus,
1603
                    // we don't need to notify any ancestors.
1604
                    this.set(focusedChildString, BrsInvalid.Instance);
×
1605
                }
1606
            }
1607

1608
            return BrsBoolean.False; //brightscript always returns false for some reason
26✔
1609
        },
1610
    });
1611

1612
    /**
1613
     *  Returns true if the subject node or any of its descendants in the SceneGraph node tree
1614
     *  has remote control focus
1615
     */
1616
    private isinfocuschain = new Callable("isinfocuschain", {
931✔
1617
        signature: {
1618
            args: [],
1619
            returns: ValueKind.Boolean,
1620
        },
1621
        impl: (interpreter: Interpreter) => {
1622
            // loop through all children DFS and check if any children has focus
1623
            if (interpreter.environment.getFocusedNode() === this) {
58✔
1624
                return BrsBoolean.True;
10✔
1625
            }
1626

1627
            return BrsBoolean.from(this.isChildrenFocused(interpreter));
48✔
1628
        },
1629
    });
1630

1631
    /* Returns the node that is a descendant of the nearest component ancestor of the subject node whose id field matches the given name,
1632
        otherwise return invalid.
1633
        Implemented as a DFS from the top of parent hierarchy to match the observed behavior as opposed to the BFS mentioned in the docs. */
1634
    private findnode = new Callable("findnode", {
931✔
1635
        signature: {
1636
            args: [new StdlibArgument("name", ValueKind.String)],
1637
            returns: ValueKind.Dynamic,
1638
        },
1639
        impl: (interpreter: Interpreter, name: BrsString) => {
1640
            // Roku's implementation returns invalid on empty string
1641
            if (name.value.length === 0) return BrsInvalid.Instance;
46✔
1642

1643
            // climb parent hierarchy to find node to start search at
1644
            let root: RoSGNode = this;
44✔
1645
            while (root.parent && root.parent instanceof RoSGNode) {
44✔
1646
                root = root.parent;
10✔
1647
            }
1648

1649
            // perform search
1650
            return this.findNodeById(root, name);
44✔
1651
        },
1652
    });
1653

1654
    /* Checks whether the subtype of the subject node is a descendant of the subtype nodeType
1655
     * in the SceneGraph node class hierarchy.
1656
     *
1657
     *
1658
     */
1659
    private issubtype = new Callable("issubtype", {
931✔
1660
        signature: {
1661
            args: [new StdlibArgument("nodeType", ValueKind.String)],
1662
            returns: ValueKind.Boolean,
1663
        },
1664
        impl: (interpreter: Interpreter, nodeType: BrsString) => {
1665
            return BrsBoolean.from(isSubtypeCheck(this.nodeSubtype, nodeType.value));
15✔
1666
        },
1667
    });
1668

1669
    /* Checks whether the subtype of the subject node is a descendant of the subtype nodeType
1670
     * in the SceneGraph node class hierarchy.
1671
     */
1672
    private parentsubtype = new Callable("parentsubtype", {
931✔
1673
        signature: {
1674
            args: [new StdlibArgument("nodeType", ValueKind.String)],
1675
            returns: ValueKind.Object,
1676
        },
1677
        impl: (interpreter: Interpreter, nodeType: BrsString) => {
1678
            const parentType = subtypeHierarchy.get(nodeType.value.toLowerCase());
4✔
1679
            if (parentType) {
4✔
1680
                return new BrsString(parentType);
3✔
1681
            }
1682
            return BrsInvalid.Instance;
1✔
1683
        },
1684
    });
1685

1686
    /* Returns a Boolean value indicating whether the roSGNode parameter
1687
            refers to the same node object as this node */
1688
    private issamenode = new Callable("issamenode", {
931✔
1689
        signature: {
1690
            args: [new StdlibArgument("roSGNode", ValueKind.Dynamic)],
1691
            returns: ValueKind.Boolean,
1692
        },
1693
        impl: (interpreter: Interpreter, roSGNode: RoSGNode) => {
1694
            return BrsBoolean.from(this === roSGNode);
4✔
1695
        },
1696
    });
1697

1698
    /* Returns the subtype of this node as specified when it was created */
1699
    private subtype = new Callable("subtype", {
931✔
1700
        signature: {
1701
            args: [],
1702
            returns: ValueKind.String,
1703
        },
1704
        impl: (interpreter: Interpreter) => {
1705
            return new BrsString(this.nodeSubtype);
38✔
1706
        },
1707
    });
1708

1709
    /* Takes a list of models and creates fields with default values, and adds them to this.fields. */
1710
    protected registerDefaultFields(fields: FieldModel[]) {
1711
        fields.forEach((field) => {
1,191✔
1712
            let value = getBrsValueFromFieldType(field.type, field.value);
10,324✔
1713
            let fieldType = FieldKind.fromString(field.type);
10,324✔
1714
            if (fieldType) {
10,324✔
1715
                this.fields.set(
10,324✔
1716
                    field.name.toLowerCase(),
1717
                    new Field(value, fieldType, !!field.alwaysNotify, field.hidden)
1718
                );
1719
            }
1720
        });
1721
    }
1722

1723
    /**
1724
     * Takes a list of preset fields and creates fields from them.
1725
     * TODO: filter out any non-allowed members. For example, if we instantiate a Node like this:
1726
     *      <Node thisisnotanodefield="fakevalue" />
1727
     * then Roku logs an error, because Node does not have a property called "thisisnotanodefield".
1728
     */
1729
    protected registerInitializedFields(fields: AAMember[]) {
1730
        fields.forEach((field) => {
1,161✔
1731
            let fieldType = FieldKind.fromBrsType(field.value);
503✔
1732
            if (fieldType) {
503✔
1733
                this.fields.set(
502✔
1734
                    field.name.value.toLowerCase(),
1735
                    new Field(field.value, fieldType, false)
1736
                );
1737
            }
1738
        });
1739
    }
1740
}
1741

1742
// A node that represents the m.global, referenced by all other nodes
1743
export const mGlobal = new RoSGNode([]);
142✔
1744

1745
export function createNodeByType(interpreter: Interpreter, type: BrsString): RoSGNode | BrsInvalid {
142✔
1746
    // If the requested component has been mocked, construct and return the mock instead
1747
    let maybeMock = interpreter.environment.getMockObject(type.value.toLowerCase());
231✔
1748
    if (maybeMock instanceof RoAssociativeArray) {
231✔
1749
        let mock: typeof MockNodeModule = require("../../extensions/MockNode");
1✔
1750
        return new mock.MockNode(maybeMock, type.value);
1✔
1751
    }
1752

1753
    // If this is a built-in component, then return it.
1754
    let component = ComponentFactory.createComponent(type.value as BrsComponentName);
230✔
1755
    if (component) {
230✔
1756
        return component;
156✔
1757
    }
1758

1759
    let typeDef = interpreter.environment.nodeDefMap.get(type.value.toLowerCase());
74✔
1760
    if (typeDef) {
74✔
1761
        //use typeDef object to tack on all the bells & whistles of a custom node
1762
        let typeDefStack: ComponentDefinition[] = [];
72✔
1763
        let currentEnv = typeDef.environment?.createSubEnvironment();
72!
1764

1765
        // Adding all component extensions to the stack to call init methods
1766
        // in the correct order.
1767
        typeDefStack.push(typeDef);
72✔
1768
        while (typeDef) {
72✔
1769
            // Add the current typedef to the subtypeHierarchy
1770
            subtypeHierarchy.set(typeDef.name!.toLowerCase(), typeDef.extends || "Node");
81✔
1771

1772
            typeDef = interpreter.environment.nodeDefMap.get(typeDef.extends?.toLowerCase());
81✔
1773
            if (typeDef) typeDefStack.push(typeDef);
81✔
1774
        }
1775

1776
        // Start from the "basemost" component of the tree.
1777
        typeDef = typeDefStack.pop();
72✔
1778

1779
        // If this extends a built-in component, create it.
1780
        let node = ComponentFactory.createComponent(
72✔
1781
            typeDef!.extends as BrsComponentName,
1782
            type.value
1783
        );
1784

1785
        // Default to Node as parent.
1786
        if (!node) {
72✔
1787
            node = new RoSGNode([], type.value);
68✔
1788
        }
1789
        let mPointer = new RoAssociativeArray([]);
72✔
1790
        currentEnv?.setM(new RoAssociativeArray([]));
72!
1791

1792
        // Add children, fields and call each init method starting from the
1793
        // "basemost" component of the tree.
1794
        while (typeDef) {
72✔
1795
            let init: BrsType;
1796

1797
            interpreter.inSubEnv((subInterpreter) => {
81✔
1798
                addChildren(subInterpreter, node!, typeDef!);
81✔
1799
                addFields(subInterpreter, node!, typeDef!);
81✔
1800
                return BrsInvalid.Instance;
81✔
1801
            }, currentEnv);
1802

1803
            interpreter.inSubEnv((subInterpreter) => {
81✔
1804
                init = subInterpreter.getInitMethod();
81✔
1805
                return BrsInvalid.Instance;
81✔
1806
            }, typeDef.environment);
1807

1808
            interpreter.inSubEnv((subInterpreter) => {
81✔
1809
                subInterpreter.environment.hostNode = node;
81✔
1810

1811
                mPointer.set(new BrsString("top"), node!);
81✔
1812
                mPointer.set(new BrsString("global"), mGlobal);
81✔
1813
                subInterpreter.environment.setM(mPointer);
81✔
1814
                subInterpreter.environment.setRootM(mPointer);
81✔
1815
                node!.m = mPointer;
81✔
1816
                if (init instanceof Callable) {
81✔
1817
                    init.call(subInterpreter);
35✔
1818
                }
1819
                return BrsInvalid.Instance;
81✔
1820
            }, currentEnv);
1821

1822
            typeDef = typeDefStack.pop();
81✔
1823
        }
1824

1825
        return node;
72✔
1826
    } else {
1827
        return BrsInvalid.Instance;
2✔
1828
    }
1829
}
1830

1831
function addFields(interpreter: Interpreter, node: RoSGNode, typeDef: ComponentDefinition) {
1832
    let fields = typeDef.fields;
81✔
1833
    for (let [key, value] of Object.entries(fields)) {
81✔
1834
        if (value instanceof Object) {
53✔
1835
            // Roku throws a run-time error if any fields are duplicated between inherited components.
1836
            // TODO: throw exception when fields are duplicated.
1837
            let fieldName = new BrsString(key);
53✔
1838

1839
            let addField = node.getMethod("addField");
53✔
1840
            if (addField) {
53✔
1841
                addField.call(
53✔
1842
                    interpreter,
1843
                    fieldName,
1844
                    new BrsString(value.type),
1845
                    BrsBoolean.from(value.alwaysNotify === "true")
1846
                );
1847
            }
1848

1849
            // set default value if it was specified in xml
1850
            let setField = node.getMethod("setField");
53✔
1851
            if (setField && value.value) {
53✔
1852
                setField.call(
31✔
1853
                    interpreter,
1854
                    fieldName,
1855
                    getBrsValueFromFieldType(value.type, value.value)
1856
                );
1857
            }
1858

1859
            // Add the onChange callback if it exists.
1860
            if (value.onChange) {
53✔
1861
                let field = node.getFields().get(fieldName.value.toLowerCase());
14✔
1862
                let callableFunction = interpreter.getCallableFunction(value.onChange);
14✔
1863
                if (callableFunction && field) {
14✔
1864
                    // observers set via `onChange` can never be removed, despite RBI's documentation claiming
1865
                    // that "[i]t is equivalent to calling the ifSGNodeField observeField() method".
1866
                    field.addObserver(
13✔
1867
                        "permanent",
1868
                        interpreter,
1869
                        callableFunction,
1870
                        node,
1871
                        node,
1872
                        fieldName
1873
                    );
1874
                }
1875
            }
1876
        }
1877
    }
1878
}
1879

1880
function addChildren(
1881
    interpreter: Interpreter,
1882
    node: RoSGNode,
1883
    typeDef: ComponentDefinition | ComponentNode
1884
) {
1885
    let children = typeDef.children;
98✔
1886
    let appendChild = node.getMethod("appendchild");
98✔
1887

1888
    children.forEach((child) => {
98✔
1889
        let newChild = createNodeByType(interpreter, new BrsString(child.name));
122✔
1890
        if (newChild instanceof RoSGNode) {
122✔
1891
            if (appendChild) {
120✔
1892
                appendChild.call(interpreter, newChild);
120✔
1893
                let setField = newChild.getMethod("setfield");
120✔
1894
                if (setField) {
120✔
1895
                    let nodeFields = newChild.getFields();
120✔
1896
                    for (let [key, value] of Object.entries(child.fields)) {
120✔
1897
                        let field = nodeFields.get(key.toLowerCase());
304✔
1898
                        if (field) {
304✔
1899
                            setField.call(
304✔
1900
                                interpreter,
1901
                                new BrsString(key),
1902
                                // use the field type to construct the field value
1903
                                getBrsValueFromFieldType(field.getType(), value)
1904
                            );
1905
                        }
1906
                    }
1907
                }
1908
            }
1909

1910
            if (child.children.length > 0) {
120✔
1911
                // we need to add the child's own children
1912
                addChildren(interpreter, newChild, child);
17✔
1913
            }
1914
        }
1915
    });
1916
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc