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

rokucommunity / brs / #213

27 Jan 2025 05:30PM UTC coverage: 86.996% (-2.2%) from 89.205%
#213

push

web-flow
Implemented several improvements to SceneGraph (#87)

* Implemented several improvements to SceneGraph

* Fixed most test cases

* Reduced unnecessary code

* Fixed typo

* Added Warning when trying to create a non-existent Node

* Fixed parser

* Fixed unit tests

* Implemented support for `infoFields`

* Prettier fix

* Simplified execute callback code and matched behavior with Roku

* Adding comment to clarify the exception

2240 of 2807 branches covered (79.8%)

Branch coverage included in aggregate %.

139 of 304 new or added lines in 18 files covered. (45.72%)

2 existing lines in 1 file now uncovered.

6129 of 6813 relevant lines covered (89.96%)

27562.41 hits per line

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

93.54
/src/brsTypes/components/RoSGNode.ts
1
import {
143✔
2
    BrsValue,
3
    ValueKind,
4
    BrsString,
5
    BrsInvalid,
6
    BrsBoolean,
7
    Uninitialized,
8
    getBrsValueFromFieldType,
9
    getValueKindFromFieldType,
10
} from "../BrsType";
11
import { RoSGNodeEvent } from "./RoSGNodeEvent";
143✔
12
import { BrsComponent, BrsIterable } from "./BrsComponent";
143✔
13
import { BrsType, isBrsNumber, isBrsString, toAssociativeArray } from "..";
143✔
14
import { Callable, StdlibArgument } from "../Callable";
143✔
15
import { Interpreter } from "../../interpreter";
16
import { Int32 } from "../Int32";
143✔
17
import { Int64 } from "../Int64";
143✔
18
import { Float } from "../Float";
143✔
19
import { Double } from "../Double";
143✔
20
import { RoAssociativeArray } from "./RoAssociativeArray";
143✔
21
import { RoArray } from "./RoArray";
143✔
22
import { AAMember } from "./RoAssociativeArray";
23
import { ComponentDefinition, ComponentNode } from "../../scenegraph";
24
import { NodeFactory, BrsNodeType } from "../nodes/NodeFactory";
143✔
25
import { Environment, Scope } from "../../interpreter/Environment";
143✔
26
import { RoInvalid } from "./RoInvalid";
143✔
27
import type * as MockNodeModule from "../../extensions/MockNode";
28
import { BlockEnd } from "../../parser/Statement";
143✔
29
import { Stmt } from "../../parser";
143✔
30
import { generateArgumentMismatchError } from "../../interpreter/ArgumentMismatch";
143✔
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
        infoFields?: RoArray;
41
    };
42
}
43

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

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

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

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

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

124
export class Field {
143✔
125
    private permanentObservers: BrsCallback[] = [];
11,053✔
126
    private unscopedObservers: BrsCallback[] = [];
11,053✔
127
    private scopedObservers: Map<RoSGNode, BrsCallback[]> = new Map();
11,053✔
128

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

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

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

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

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

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

163
        return this.value;
622✔
164
    }
165

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

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

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

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

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

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

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

239
    removeUnscopedObservers() {
240
        this.unscopedObservers.splice(0);
2✔
241
    }
242

243
    removeScopedObservers(hostNode: RoSGNode) {
244
        this.scopedObservers.get(hostNode)?.splice(0);
2!
245
        this.scopedObservers.delete(hostNode);
2✔
246
    }
247

248
    private executeCallbacks(callback: BrsCallback) {
249
        const { interpreter, callable, hostNode, environment, eventParams } = callback;
94✔
250

251
        // Get info fields current value, if exists.
252
        let infoFields: RoAssociativeArray | undefined;
253
        if (eventParams.infoFields) {
94✔
254
            const fieldsMap = new Map();
68✔
255
            eventParams.infoFields.elements?.forEach((element) => {
68!
NEW
256
                if (isBrsString(element)) {
×
257
                    // TODO: Check how to handle object values (by reference or by value)
NEW
258
                    fieldsMap.set(element.value, hostNode.get(element));
×
259
                }
260
            });
261
            infoFields = toAssociativeArray(fieldsMap);
68✔
262
        }
263

264
        // Every time a callback happens, a new event is created.
265
        let event = new RoSGNodeEvent(
94✔
266
            eventParams.node,
267
            eventParams.fieldName,
268
            this.value,
269
            infoFields
270
        );
271

272
        interpreter.inSubEnv((subInterpreter) => {
94✔
273
            subInterpreter.environment.hostNode = hostNode;
94✔
274
            subInterpreter.environment.setRootM(hostNode.m);
94✔
275

276
            try {
94✔
277
                // Check whether the callback is expecting an event parameter.
278
                const satisfiedSignature = callable.getFirstSatisfiedSignature([event]);
94✔
279
                if (satisfiedSignature) {
94✔
280
                    let { signature, impl } = satisfiedSignature;
81✔
281
                    subInterpreter.environment.define(
81✔
282
                        Scope.Function,
283
                        signature.args[0].name.text,
284
                        event
285
                    );
286
                    impl(subInterpreter, event);
81✔
287
                } else {
288
                    // Check whether the callback has a signature without parameters.
289
                    // Silently ignore if the callback has no signature that matches.
290
                    callable.getFirstSatisfiedSignature([])?.impl(subInterpreter);
13!
291
                }
292
            } catch (err) {
293
                if (!(err instanceof BlockEnd)) {
1!
294
                    throw err;
×
295
                }
296
            }
297
            return BrsInvalid.Instance;
94✔
298
        }, environment);
299
    }
300
}
301

302
/* Hierarchy of all node Types. Used to discover is a current node is a subtype of another node */
303
const subtypeHierarchy = new Map<string, string>();
143✔
304

305
/**
306
 *  Checks the node sub type hierarchy to see if the current node is a sub component of the given node type
307
 *
308
 * @param {string} currentNodeType
309
 * @param {string} checkType
310
 * @returns {boolean}
311
 */
312
function isSubtypeCheck(currentNodeType: string, checkType: string): boolean {
313
    checkType = checkType.toLowerCase();
35✔
314
    currentNodeType = currentNodeType.toLowerCase();
35✔
315
    if (currentNodeType === checkType) {
35✔
316
        return true;
12✔
317
    }
318
    let nextNodeType = subtypeHierarchy.get(currentNodeType);
23✔
319
    if (nextNodeType == null) {
23✔
320
        return false;
3✔
321
    }
322
    return isSubtypeCheck(nextNodeType, checkType);
20✔
323
}
324

325
export class RoSGNode extends BrsComponent implements BrsValue, BrsIterable {
143✔
326
    readonly kind = ValueKind.Object;
947✔
327
    private fields = new Map<string, Field>();
947✔
328
    private children: RoSGNode[] = [];
947✔
329
    private parent: RoSGNode | BrsInvalid = BrsInvalid.Instance;
947✔
330

331
    readonly defaultFields: FieldModel[] = [
947✔
332
        { name: "id", type: "string" },
333
        { name: "focusedchild", type: "node", alwaysNotify: true },
334
        { name: "focusable", type: "boolean" },
335
        { name: "change", type: "roAssociativeArray" },
336
    ];
337
    m: RoAssociativeArray = new RoAssociativeArray([]);
947✔
338

339
    constructor(initializedFields: AAMember[], readonly nodeSubtype: string = "Node") {
947✔
340
        super("Node");
947✔
341
        this.setExtendsType();
947✔
342

343
        // All nodes start have some built-in fields when created.
344
        this.registerDefaultFields(this.defaultFields);
947✔
345

346
        // After registering default fields, then register fields instantiated with initial values.
347
        this.registerInitializedFields(initializedFields);
947✔
348

349
        this.registerMethods({
947✔
350
            ifAssociativeArray: [
351
                this.clear,
352
                this.delete,
353
                this.addreplace,
354
                this.count,
355
                this.doesexist,
356
                this.append,
357
                this.keys,
358
                this.items,
359
                this.lookup,
360
                this.lookupCI,
361
            ],
362
            ifSGNodeField: [
363
                this.addfield,
364
                this.addfields,
365
                this.getfield,
366
                this.getfields,
367
                this.hasfield,
368
                this.observefield,
369
                this.unobserveField,
370
                this.observeFieldScoped,
371
                this.unobserveFieldScoped,
372
                this.removefield,
373
                this.setfield,
374
                this.setfields,
375
                this.update,
376
            ],
377
            ifSGNodeChildren: [
378
                this.appendchild,
379
                this.getchildcount,
380
                this.getchildren,
381
                this.removechild,
382
                this.getparent,
383
                this.createchild,
384
                this.replacechild,
385
                this.removechildren,
386
                this.appendchildren,
387
                this.getchild,
388
                this.insertchild,
389
                this.removechildrenindex,
390
                this.removechildindex,
391
                this.reparent,
392
                this.createchildren,
393
                this.replacechildren,
394
                this.insertchildren,
395
            ],
396
            ifSGNodeFocus: [this.hasfocus, this.setfocus, this.isinfocuschain],
397
            ifSGNodeDict: [
398
                this.findnode,
399
                this.issamenode,
400
                this.subtype,
401
                this.callfunc,
402
                this.issubtype,
403
                this.parentsubtype,
404
            ],
405
            ifSGNodeBoundingRect: [this.boundingRect],
406
        });
407
    }
408

409
    toString(parent?: BrsType): string {
410
        let componentName = "roSGNode:" + this.nodeSubtype;
27✔
411

412
        if (parent) {
27✔
413
            return `<Component: ${componentName}>`;
6✔
414
        }
415

416
        return [
21✔
417
            `<Component: ${componentName}> =`,
418
            "{",
419
            ...Array.from(this.fields.entries())
420
                .reverse()
421
                .map(([key, value]) => `    ${key}: ${value.toString(this)}`),
403✔
422
            "}",
423
        ].join("\n");
424
    }
425

426
    equalTo(other: BrsType) {
427
        // SceneGraph nodes are never equal to anything
428
        return BrsBoolean.False;
7✔
429
    }
430

431
    getElements() {
432
        return Array.from(this.fields.keys())
6✔
433
            .sort()
434
            .map((key) => new BrsString(key));
36✔
435
    }
436

437
    getValues() {
438
        return Array.from(this.fields.values())
×
439
            .sort()
440
            .map((field: Field) => field.getValue());
×
441
    }
442

443
    getFields() {
444
        return this.fields;
189✔
445
    }
446

447
    get(index: BrsType) {
448
        if (index.kind !== ValueKind.String) {
926!
449
            throw new Error("RoSGNode indexes must be strings");
×
450
        }
451

452
        // TODO: this works for now, in that a property with the same name as a method essentially
453
        // overwrites the method. The only reason this doesn't work is that getting a method from an
454
        // associative array and _not_ calling it returns `invalid`, but calling it returns the
455
        // function itself. I'm not entirely sure why yet, but it's gotta have something to do with
456
        // how methods are implemented within RBI.
457
        //
458
        // Are they stored separately from elements, like they are here? Or does
459
        // `Interpreter#visitCall` need to check for `invalid` in its callable, then try to find a
460
        // method with the desired name separately? That last bit would work but it's pretty gross.
461
        // That'd allow roArrays to have methods with the methods not accessible via `arr["count"]`.
462
        // Same with RoAssociativeArrays I guess.
463
        let field = this.fields.get(index.value.toLowerCase());
926✔
464
        if (field) {
926✔
465
            return field.getValue();
614✔
466
        }
467
        return this.getMethod(index.value) || BrsInvalid.Instance;
312✔
468
    }
469

470
    set(index: BrsType, value: BrsType, alwaysNotify: boolean = false, kind?: FieldKind) {
591✔
471
        if (index.kind !== ValueKind.String) {
656!
472
            throw new Error("RoSGNode indexes must be strings");
×
473
        }
474

475
        let mapKey = index.value.toLowerCase();
656✔
476
        let fieldType = kind || FieldKind.fromBrsType(value);
656✔
477
        let field = this.fields.get(mapKey);
656✔
478

479
        if (!field) {
656✔
480
            // RBI does not create a new field if the value isn't valid.
481
            if (fieldType) {
121✔
482
                field = new Field(value, fieldType, alwaysNotify);
119✔
483
                this.fields.set(mapKey, field);
119✔
484
            }
485
        } else if (field.canAcceptValue(value)) {
535✔
486
            // Fields are not overwritten if they haven't the same type.
487
            field.setValue(value);
530✔
488
            this.fields.set(mapKey, field);
530✔
489
        }
490

491
        return BrsInvalid.Instance;
656✔
492
    }
493

494
    getParent() {
495
        return this.parent;
7✔
496
    }
497

498
    setParent(parent: RoSGNode) {
499
        this.parent = parent;
336✔
500
    }
501

502
    removeParent() {
503
        this.parent = BrsInvalid.Instance;
51✔
504
    }
505

506
    // recursively search for any child that's focused via DFS
507
    isChildrenFocused(interpreter: Interpreter): boolean {
508
        if (this.children.length === 0) {
142✔
509
            return false;
55✔
510
        }
511

512
        for (let childNode of this.children) {
87✔
513
            if (interpreter.environment.getFocusedNode() === childNode) {
116✔
514
                return true;
22✔
515
            } else if (childNode.isChildrenFocused(interpreter)) {
94✔
516
                return true;
18✔
517
            }
518
        }
519
        return false;
47✔
520
    }
521

522
    /* searches the node tree for a node with the given id */
523
    private findNodeById(node: RoSGNode, id: BrsString): RoSGNode | BrsInvalid {
524
        // test current node in tree
525
        let currentId = node.get(new BrsString("id"));
163✔
526
        if (currentId.toString() === id.toString()) {
163✔
527
            return node;
42✔
528
        }
529

530
        // visit each child
531
        for (let child of node.children) {
121✔
532
            let result = this.findNodeById(child, id);
119✔
533
            if (result instanceof RoSGNode) {
119✔
534
                return result;
63✔
535
            }
536
        }
537

538
        // name was not found anywhere in tree
539
        return BrsInvalid.Instance;
58✔
540
    }
541

542
    private removeChildByReference(child: BrsType): boolean {
543
        if (child instanceof RoSGNode) {
73✔
544
            let spliceIndex = this.children.indexOf(child);
71✔
545
            if (spliceIndex >= 0) {
71✔
546
                child.removeParent();
34✔
547
                this.children.splice(spliceIndex, 1);
34✔
548
            }
549
            return true;
71✔
550
        }
551
        return false;
2✔
552
    }
553

554
    private appendChildToParent(child: BrsType): boolean {
555
        if (child instanceof RoSGNode) {
286✔
556
            if (this.children.includes(child)) {
285✔
557
                return true;
1✔
558
            }
559
            this.children.push(child);
284✔
560
            child.setParent(this);
284✔
561
            return true;
284✔
562
        }
563
        return false;
1✔
564
    }
565

566
    private replaceChildAtIndex(newchild: BrsType, index: Int32): boolean {
567
        let childrenSize = this.children.length;
23✔
568
        let indexValue = index.getValue();
23✔
569
        if (newchild instanceof RoSGNode && indexValue < childrenSize) {
23✔
570
            // If newchild is already a child, remove it first.
571
            this.removeChildByReference(newchild);
17✔
572
            if (indexValue >= 0) {
17✔
573
                // The check is done to see if indexValue is inside the
574
                // new length of this.children (in case newchild was
575
                // removed above)
576
                if (indexValue < this.children.length) {
13✔
577
                    // Remove the parent of the child at indexValue
578
                    this.children[indexValue].removeParent();
12✔
579
                }
580
                newchild.setParent(this);
13✔
581
                this.children.splice(indexValue, 1, newchild);
13✔
582
            }
583
            return true;
17✔
584
        }
585
        return false;
6✔
586
    }
587

588
    private insertChildAtIndex(child: BrsType, index: Int32): boolean {
589
        if (child instanceof RoSGNode) {
18✔
590
            let childrenSize = this.children.length;
18✔
591
            let indexValue = index.getValue() < 0 ? childrenSize : index.getValue();
18✔
592
            // Remove node if it already exists
593
            this.removeChildByReference(child);
18✔
594
            child.setParent(this);
18✔
595
            this.children.splice(indexValue, 0, child);
18✔
596
            return true;
18✔
597
        }
598
        return false;
×
599
    }
600

601
    /* used for isSubtype */
602
    protected setExtendsType() {
603
        let baseClass = this.constructor;
947✔
604
        let currentNodeType: string, parentType: string;
605
        while (baseClass) {
947✔
606
            currentNodeType = baseClass.name.toLowerCase();
1,237✔
607

608
            const parentClass = Object.getPrototypeOf(baseClass);
1,237✔
609

610
            if (parentClass && parentClass !== Object && parentClass.name) {
1,237!
611
                baseClass = parentClass;
1,237✔
612
                parentType = parentClass.name;
1,237✔
613
                if (parentType === "BrsComponent") {
1,237✔
614
                    // Only care about RoSgNode and above
615
                    break;
947✔
616
                }
617
                if (parentType === "RoSGNode") {
290✔
618
                    // RoSGNode is referenced as "Node"
619
                    parentType = "Node";
184✔
620
                }
621
                if (!subtypeHierarchy.has(currentNodeType)) {
290✔
622
                    subtypeHierarchy.set(currentNodeType, parentType);
60✔
623
                }
624
            } else {
625
                break;
×
626
            }
627
        }
628
    }
629

630
    /**
631
     * Calls the function specified on this node.
632
     */
633
    private callfunc = new Callable(
947✔
634
        "callfunc",
635
        ...Callable.variadic({
636
            signature: {
637
                args: [new StdlibArgument("functionname", ValueKind.String)],
638
                returns: ValueKind.Dynamic,
639
            },
640
            impl: (
641
                interpreter: Interpreter,
642
                functionName: BrsString,
643
                ...functionArgs: BrsType[]
644
            ) => {
645
                // We need to search the callee's environment for this function rather than the caller's.
646
                let componentDef = interpreter.environment.nodeDefMap.get(
18✔
647
                    this.nodeSubtype.toLowerCase()
648
                );
649

650
                // Only allow public functions (defined in the interface) to be called.
651
                if (componentDef && functionName.value in componentDef.functions) {
18✔
652
                    // Use the mocked component functions instead of the real one, if it's a mocked component.
653
                    if (interpreter.environment.isMockedObject(this.nodeSubtype.toLowerCase())) {
17✔
654
                        let maybeMethod = this.getMethod(functionName.value);
1✔
655
                        return (
1✔
656
                            maybeMethod?.call(interpreter, ...functionArgs) || BrsInvalid.Instance
4!
657
                        );
658
                    }
659

660
                    return interpreter.inSubEnv((subInterpreter) => {
16✔
661
                        let functionToCall = subInterpreter.getCallableFunction(functionName.value);
16✔
662
                        if (!functionToCall) {
16✔
663
                            interpreter.stderr.write(
1✔
664
                                `Ignoring attempt to call non-implemented function ${functionName}`
665
                            );
666
                            return BrsInvalid.Instance;
1✔
667
                        }
668

669
                        subInterpreter.environment.setM(this.m);
15✔
670
                        subInterpreter.environment.setRootM(this.m);
15✔
671
                        subInterpreter.environment.hostNode = this;
15✔
672

673
                        try {
15✔
674
                            // Determine whether the function should get arguments or not.
675
                            let satisfiedSignature =
676
                                functionToCall.getFirstSatisfiedSignature(functionArgs);
15✔
677
                            let args = satisfiedSignature ? functionArgs : [];
15✔
678
                            if (!satisfiedSignature) {
15✔
679
                                satisfiedSignature = functionToCall.getFirstSatisfiedSignature([]);
2✔
680
                            }
681
                            if (satisfiedSignature) {
15✔
682
                                const funcLoc =
683
                                    functionToCall.getLocation() ?? interpreter.location;
13✔
684
                                interpreter.addToStack({
13✔
685
                                    functionName: functionName.value,
686
                                    functionLocation: funcLoc,
687
                                    callLocation: funcLoc,
688
                                    signature: satisfiedSignature.signature,
689
                                });
690
                                try {
13✔
691
                                    const returnValue = functionToCall.call(
13✔
692
                                        subInterpreter,
693
                                        ...args
694
                                    );
695
                                    interpreter.stack.pop();
3✔
696
                                    return returnValue;
3✔
697
                                } catch (err) {
698
                                    throw err;
10✔
699
                                }
700
                            } else {
701
                                return interpreter.addError(
2✔
702
                                    generateArgumentMismatchError(
703
                                        functionToCall,
704
                                        functionArgs,
705
                                        interpreter.stack[interpreter.stack.length - 1]
706
                                            .functionLocation
707
                                    )
708
                                );
709
                            }
710
                        } catch (reason) {
711
                            if (!(reason instanceof Stmt.ReturnValue)) {
12✔
712
                                // re-throw interpreter errors
713
                                throw reason;
2✔
714
                            }
715
                            return reason.value || BrsInvalid.Instance;
10!
716
                        }
717
                    }, componentDef.environment);
718
                }
719

720
                interpreter.stderr.write(
1✔
721
                    `Warning calling function in ${this.nodeSubtype}: no function interface specified for ${functionName}`
722
                );
723
                return BrsInvalid.Instance;
1✔
724
            },
725
        })
726
    );
727

728
    /** Removes all fields from the node */
729
    // ToDo: Built-in fields shouldn't be removed
730
    private clear = new Callable("clear", {
947✔
731
        signature: {
732
            args: [],
733
            returns: ValueKind.Void,
734
        },
735
        impl: (interpreter: Interpreter) => {
736
            this.fields.clear();
2✔
737
            return BrsInvalid.Instance;
2✔
738
        },
739
    });
740

741
    /** Removes a given item from the node */
742
    // ToDo: Built-in fields shouldn't be removed
743
    private delete = new Callable("delete", {
947✔
744
        signature: {
745
            args: [new StdlibArgument("str", ValueKind.String)],
746
            returns: ValueKind.Boolean,
747
        },
748
        impl: (interpreter: Interpreter, str: BrsString) => {
749
            this.fields.delete(str.value.toLowerCase());
5✔
750
            return BrsBoolean.True; //RBI always returns true
5✔
751
        },
752
    });
753

754
    /** Given a key and value, adds an item to the node if it doesn't exist
755
     * Or replaces the value of a key that already exists in the node
756
     */
757
    private addreplace = new Callable("addreplace", {
947✔
758
        signature: {
759
            args: [
760
                new StdlibArgument("key", ValueKind.String),
761
                new StdlibArgument("value", ValueKind.Dynamic),
762
            ],
763
            returns: ValueKind.Void,
764
        },
765
        impl: (interpreter: Interpreter, key: BrsString, value: BrsType) => {
766
            this.set(key, value);
3✔
767
            return BrsInvalid.Instance;
3✔
768
        },
769
    });
770

771
    /** Returns the number of items in the node */
772
    protected count = new Callable("count", {
947✔
773
        signature: {
774
            args: [],
775
            returns: ValueKind.Int32,
776
        },
777
        impl: (interpreter: Interpreter) => {
778
            return new Int32(this.fields.size);
5✔
779
        },
780
    });
781

782
    /** Returns a boolean indicating whether or not a given key exists in the node */
783
    private doesexist = new Callable("doesexist", {
947✔
784
        signature: {
785
            args: [new StdlibArgument("str", ValueKind.String)],
786
            returns: ValueKind.Boolean,
787
        },
788
        impl: (interpreter: Interpreter, str: BrsString) => {
789
            return this.get(str) !== BrsInvalid.Instance ? BrsBoolean.True : BrsBoolean.False;
5✔
790
        },
791
    });
792

793
    /** Appends a new node to another. If two keys are the same, the value of the original AA is replaced with the new one. */
794
    private append = new Callable("append", {
947✔
795
        signature: {
796
            args: [new StdlibArgument("obj", ValueKind.Object)],
797
            returns: ValueKind.Void,
798
        },
799
        impl: (interpreter: Interpreter, obj: BrsType) => {
800
            if (obj instanceof RoAssociativeArray) {
2✔
801
                obj.elements.forEach((value, key) => {
1✔
802
                    let fieldType = FieldKind.fromBrsType(value);
1✔
803

804
                    // if the field doesn't have a valid value, RBI doesn't add it.
805
                    if (fieldType) {
1✔
806
                        this.fields.set(key, new Field(value, fieldType, false));
1✔
807
                    }
808
                });
809
            } else if (obj instanceof RoSGNode) {
1✔
810
                obj.getFields().forEach((value, key) => {
1✔
811
                    this.fields.set(key, value);
9✔
812
                });
813
            }
814

815
            return BrsInvalid.Instance;
2✔
816
        },
817
    });
818

819
    /** Returns an array of keys from the node in lexicographical order */
820
    protected keys = new Callable("keys", {
947✔
821
        signature: {
822
            args: [],
823
            returns: ValueKind.Object,
824
        },
825
        impl: (interpreter: Interpreter) => {
826
            return new RoArray(this.getElements());
3✔
827
        },
828
    });
829

830
    /** Returns an array of key/value pairs in lexicographical order of key. */
831
    protected items = new Callable("items", {
947✔
832
        signature: {
833
            args: [],
834
            returns: ValueKind.Object,
835
        },
836
        impl: (interpreter: Interpreter) => {
837
            return new RoArray(
3✔
838
                this.getElements().map((key: BrsString) => {
839
                    return toAssociativeArray({ key: key, value: this.get(key) });
18✔
840
                })
841
            );
842
        },
843
    });
844

845
    /** Given a key, returns the value associated with that key. This method is case insensitive. */
846
    private lookup = new Callable("lookup", {
947✔
847
        signature: {
848
            args: [new StdlibArgument("key", ValueKind.String)],
849
            returns: ValueKind.Dynamic,
850
        },
851
        impl: (interpreter: Interpreter, key: BrsString) => {
852
            let lKey = key.value.toLowerCase();
4✔
853
            return this.get(new BrsString(lKey));
4✔
854
        },
855
    });
856

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

860
    /** Adds a new field to the node, if the field already exists it doesn't change the current value. */
861
    private addfield = new Callable("addfield", {
947✔
862
        signature: {
863
            args: [
864
                new StdlibArgument("fieldname", ValueKind.String),
865
                new StdlibArgument("type", ValueKind.String),
866
                new StdlibArgument("alwaysnotify", ValueKind.Boolean),
867
            ],
868
            returns: ValueKind.Boolean,
869
        },
870
        impl: (
871
            interpreter: Interpreter,
872
            fieldname: BrsString,
873
            type: BrsString,
874
            alwaysnotify: BrsBoolean
875
        ) => {
876
            let defaultValue = getBrsValueFromFieldType(type.value);
66✔
877
            let fieldKind = FieldKind.fromString(type.value);
66✔
878

879
            if (defaultValue !== Uninitialized.Instance && !this.fields.has(fieldname.value)) {
66✔
880
                this.set(fieldname, defaultValue, alwaysnotify.toBoolean(), fieldKind);
65✔
881
            }
882

883
            return BrsBoolean.True;
66✔
884
        },
885
    });
886

887
    /** Adds one or more fields defined as an associative aray of key values. */
888
    private addfields = new Callable("addfields", {
947✔
889
        signature: {
890
            args: [new StdlibArgument("fields", ValueKind.Object)],
891
            returns: ValueKind.Boolean,
892
        },
893
        impl: (interpreter: Interpreter, fields: RoAssociativeArray) => {
894
            if (!(fields instanceof RoAssociativeArray)) {
12✔
895
                return BrsBoolean.False;
1✔
896
            }
897

898
            fields.getValue().forEach((value, key) => {
11✔
899
                let fieldName = new BrsString(key);
26✔
900
                if (!this.fields.has(key)) {
26✔
901
                    this.set(fieldName, value);
24✔
902
                }
903
            });
904

905
            return BrsBoolean.True;
11✔
906
        },
907
    });
908

909
    /** Returns the value of the field passed as argument, if the field doesn't exist it returns invalid. */
910
    private getfield = new Callable("getfield", {
947✔
911
        signature: {
912
            args: [new StdlibArgument("fieldname", ValueKind.String)],
913
            returns: ValueKind.Dynamic,
914
        },
915
        impl: (interpreter: Interpreter, fieldname: BrsString) => {
916
            return this.get(fieldname);
25✔
917
        },
918
    });
919

920
    /** Returns the names and values of all the fields in the node. */
921
    private getfields = new Callable("getfields", {
947✔
922
        signature: {
923
            args: [],
924
            returns: ValueKind.Object,
925
        },
926
        impl: (interpreter: Interpreter) => {
927
            let packagedFields: AAMember[] = [];
2✔
928

929
            this.fields.forEach((field, name) => {
2✔
930
                if (field.isHidden()) {
6!
931
                    return;
×
932
                }
933

934
                packagedFields.push({
6✔
935
                    name: new BrsString(name),
936
                    value: field.getValue(),
937
                });
938
            });
939

940
            return new RoAssociativeArray(packagedFields);
2✔
941
        },
942
    });
943

944
    /** Returns true if the field exists */
945
    protected hasfield = new Callable("hasfield", {
947✔
946
        signature: {
947
            args: [new StdlibArgument("fieldname", ValueKind.String)],
948
            returns: ValueKind.Boolean,
949
        },
950
        impl: (interpreter: Interpreter, fieldname: BrsString) => {
951
            return this.fields.has(fieldname.value.toLowerCase())
7✔
952
                ? BrsBoolean.True
953
                : BrsBoolean.False;
954
        },
955
    });
956

957
    /** Registers a callback to be executed when the value of the field changes */
958
    private observefield = new Callable("observefield", {
947✔
959
        signature: {
960
            args: [
961
                new StdlibArgument("fieldname", ValueKind.String),
962
                new StdlibArgument("functionname", ValueKind.String),
963
                new StdlibArgument("infoFields", ValueKind.Object, BrsInvalid.Instance),
964
            ],
965
            returns: ValueKind.Boolean,
966
        },
967
        impl: (
968
            interpreter: Interpreter,
969
            fieldname: BrsString,
970
            functionname: BrsString,
971
            infoFields: RoArray
972
        ) => {
973
            let field = this.fields.get(fieldname.value.toLowerCase());
19✔
974
            if (field instanceof Field) {
19✔
975
                let callableFunction = interpreter.getCallableFunction(functionname.value);
18✔
976
                let subscriber = interpreter.environment.hostNode;
18✔
977
                if (!subscriber) {
18!
978
                    let location = interpreter.formatLocation();
×
979
                    interpreter.stderr.write(
×
980
                        `BRIGHTSCRIPT: ERROR: roSGNode.ObserveField: no active host node: ${location}\n`
981
                    );
982
                    return BrsBoolean.False;
×
983
                }
984

985
                if (callableFunction && subscriber) {
18!
986
                    field.addObserver(
18✔
987
                        "unscoped",
988
                        interpreter,
989
                        callableFunction,
990
                        subscriber,
991
                        this,
992
                        fieldname,
993
                        infoFields
994
                    );
995
                } else {
996
                    return BrsBoolean.False;
×
997
                }
998
            }
999
            return BrsBoolean.True;
19✔
1000
        },
1001
    });
1002

1003
    /**
1004
     * Removes all observers of a given field, regardless of whether or not the host node is the subscriber.
1005
     */
1006
    private unobserveField = new Callable("unobservefield", {
947✔
1007
        signature: {
1008
            args: [new StdlibArgument("fieldname", ValueKind.String)],
1009
            returns: ValueKind.Boolean,
1010
        },
1011
        impl: (interpreter: Interpreter, fieldname: BrsString, functionname: BrsString) => {
1012
            if (!interpreter.environment.hostNode) {
2!
1013
                let location = interpreter.formatLocation();
×
1014
                interpreter.stderr.write(
×
1015
                    `BRIGHTSCRIPT: ERROR: roSGNode.unObserveField: no active host node: ${location}\n`
1016
                );
1017
                return BrsBoolean.False;
×
1018
            }
1019

1020
            let field = this.fields.get(fieldname.value.toLowerCase());
2✔
1021
            if (field instanceof Field) {
2✔
1022
                field.removeUnscopedObservers();
2✔
1023
            }
1024
            // returns true, even if the field doesn't exist
1025
            return BrsBoolean.True;
2✔
1026
        },
1027
    });
1028

1029
    private observeFieldScoped = new Callable("observeFieldSCoped", {
947✔
1030
        signature: {
1031
            args: [
1032
                new StdlibArgument("fieldname", ValueKind.String),
1033
                new StdlibArgument("functionname", ValueKind.String),
1034
            ],
1035
            returns: ValueKind.Boolean,
1036
        },
1037
        impl: (interpreter: Interpreter, fieldname: BrsString, functionname: BrsString) => {
1038
            let field = this.fields.get(fieldname.value.toLowerCase());
6✔
1039
            if (field instanceof Field) {
6✔
1040
                let callableFunction = interpreter.getCallableFunction(functionname.value);
6✔
1041
                let subscriber = interpreter.environment.hostNode;
6✔
1042
                if (!subscriber) {
6!
1043
                    let location = interpreter.formatLocation();
×
1044
                    interpreter.stderr.write(
×
1045
                        `BRIGHTSCRIPT: ERROR: roSGNode.ObserveField: no active host node: ${location}\n`
1046
                    );
1047
                    return BrsBoolean.False;
×
1048
                }
1049

1050
                if (callableFunction && subscriber) {
6!
1051
                    field.addObserver(
6✔
1052
                        "scoped",
1053
                        interpreter,
1054
                        callableFunction,
1055
                        subscriber,
1056
                        this,
1057
                        fieldname
1058
                    );
1059
                } else {
1060
                    return BrsBoolean.False;
×
1061
                }
1062
            }
1063
            return BrsBoolean.True;
6✔
1064
        },
1065
    });
1066

1067
    private unobserveFieldScoped = new Callable("unobserveFieldScoped", {
947✔
1068
        signature: {
1069
            args: [new StdlibArgument("fieldname", ValueKind.String)],
1070
            returns: ValueKind.Boolean,
1071
        },
1072
        impl: (interpreter: Interpreter, fieldname: BrsString, functionname: BrsString) => {
1073
            if (!interpreter.environment.hostNode) {
2!
1074
                let location = interpreter.formatLocation();
×
1075
                interpreter.stderr.write(
×
1076
                    `BRIGHTSCRIPT: ERROR: roSGNode.unObserveField: no active host node: ${location}\n`
1077
                );
1078
                return BrsBoolean.False;
×
1079
            }
1080

1081
            let field = this.fields.get(fieldname.value.toLowerCase());
2✔
1082
            if (field instanceof Field) {
2✔
1083
                field.removeScopedObservers(interpreter.environment.hostNode);
2✔
1084
            }
1085
            // returns true, even if the field doesn't exist
1086
            return BrsBoolean.True;
2✔
1087
        },
1088
    });
1089

1090
    /** Removes the given field from the node */
1091
    /** TODO: node built-in fields shouldn't be removable (i.e. id, change, focusable,) */
1092
    private removefield = new Callable("removefield", {
947✔
1093
        signature: {
1094
            args: [new StdlibArgument("fieldname", ValueKind.String)],
1095
            returns: ValueKind.Boolean,
1096
        },
1097
        impl: (interpreter: Interpreter, fieldname: BrsString) => {
1098
            this.fields.delete(fieldname.value.toLowerCase());
5✔
1099
            return BrsBoolean.True; //RBI always returns true
5✔
1100
        },
1101
    });
1102

1103
    /** Updates the value of an existing field only if the types match. */
1104
    private setfield = new Callable("setfield", {
947✔
1105
        signature: {
1106
            args: [
1107
                new StdlibArgument("fieldname", ValueKind.String),
1108
                new StdlibArgument("value", ValueKind.Dynamic),
1109
            ],
1110
            returns: ValueKind.Boolean,
1111
        },
1112
        impl: (interpreter: Interpreter, fieldname: BrsString, value: BrsType) => {
1113
            let field = this.fields.get(fieldname.value.toLowerCase());
342✔
1114
            if (!field) {
342✔
1115
                return BrsBoolean.False;
1✔
1116
            }
1117

1118
            if (!field.canAcceptValue(value)) {
341✔
1119
                return BrsBoolean.False;
1✔
1120
            }
1121

1122
            this.set(fieldname, value);
340✔
1123
            return BrsBoolean.True;
340✔
1124
        },
1125
    });
1126

1127
    /** Updates the value of multiple existing field only if the types match. */
1128
    private setfields = new Callable("setfields", {
947✔
1129
        signature: {
1130
            args: [new StdlibArgument("fields", ValueKind.Object)],
1131
            returns: ValueKind.Boolean,
1132
        },
1133
        impl: (interpreter: Interpreter, fields: RoAssociativeArray) => {
1134
            if (!(fields instanceof RoAssociativeArray)) {
3!
1135
                return BrsBoolean.False;
×
1136
            }
1137

1138
            fields.getValue().forEach((value, key) => {
3✔
1139
                let fieldName = new BrsString(key);
6✔
1140
                if (this.fields.has(key)) {
6✔
1141
                    this.set(fieldName, value);
6✔
1142
                }
1143
            });
1144

1145
            return BrsBoolean.True;
3✔
1146
        },
1147
    });
1148

1149
    /* Updates the value of multiple existing field only if the types match.
1150
    In contrast to setFields method, update always return Uninitialized */
1151
    private update = new Callable("update", {
947✔
1152
        signature: {
1153
            args: [
1154
                new StdlibArgument("aa", ValueKind.Object),
1155
                new StdlibArgument("createFields", ValueKind.Boolean, BrsBoolean.False),
1156
            ],
1157
            returns: ValueKind.Uninitialized,
1158
        },
1159
        impl: (interpreter: Interpreter, aa: RoAssociativeArray, createFields: BrsBoolean) => {
1160
            if (!(aa instanceof RoAssociativeArray)) {
16!
1161
                return Uninitialized.Instance;
×
1162
            }
1163

1164
            aa.getValue().forEach((value, key) => {
16✔
1165
                let fieldName = new BrsString(key);
25✔
1166
                if (this.fields.has(key.toLowerCase()) || createFields.toBoolean()) {
25✔
1167
                    this.set(fieldName, value);
20✔
1168
                }
1169
            });
1170

1171
            return Uninitialized.Instance;
16✔
1172
        },
1173
    });
1174

1175
    /* Return the current number of children in the subject node list of children.
1176
    This is always a non-negative number. */
1177
    private getchildcount = new Callable("getchildcount", {
947✔
1178
        signature: {
1179
            args: [],
1180
            returns: ValueKind.Int32,
1181
        },
1182
        impl: (interpreter: Interpreter) => {
1183
            return new Int32(this.children.length);
44✔
1184
        },
1185
    });
1186

1187
    /* Adds a child node to the end of the subject node list of children so that it is
1188
    traversed last (of those children) during render. */
1189
    private appendchild = new Callable("appendchild", {
947✔
1190
        signature: {
1191
            args: [new StdlibArgument("child", ValueKind.Dynamic)],
1192
            returns: ValueKind.Boolean,
1193
        },
1194
        impl: (interpreter: Interpreter, child: BrsType) => {
1195
            return BrsBoolean.from(this.appendChildToParent(child));
275✔
1196
        },
1197
    });
1198

1199
    /* Retrieves the number of child nodes specified by num_children from the subject
1200
    node, starting at the position specified by index. Returns an array of the child nodes
1201
    retrieved. If num_children is -1, return all the children. */
1202
    private getchildren = new Callable("getchildren", {
947✔
1203
        signature: {
1204
            args: [
1205
                new StdlibArgument("num_children", ValueKind.Int32),
1206
                new StdlibArgument("index", ValueKind.Int32),
1207
            ],
1208
            returns: ValueKind.Object,
1209
        },
1210
        impl: (interpreter: Interpreter, num_children: Int32, index: Int32) => {
1211
            let numChildrenValue = num_children.getValue();
45✔
1212
            let indexValue = index.getValue();
45✔
1213
            let childrenSize = this.children.length;
45✔
1214
            if (numChildrenValue <= -1 && indexValue === 0) {
45✔
1215
                //short hand to return all children
1216
                return new RoArray(this.children.slice());
40✔
1217
            } else if (numChildrenValue <= 0 || indexValue < 0 || indexValue >= childrenSize) {
5✔
1218
                //these never return any children
1219
                return new RoArray([]);
2✔
1220
            } else {
1221
                //only valid cases
1222
                return new RoArray(this.children.slice(indexValue, indexValue + numChildrenValue));
3✔
1223
            }
1224

1225
            return new RoArray([]);
×
1226
        },
1227
    });
1228

1229
    /* Finds a child node in the subject node list of children, and if found,
1230
    remove it from the list of children. The match is made on the basis of actual
1231
    object identity, that is, the value of the pointer to the child node.
1232
    return false if trying to remove anything that's not a node */
1233
    private removechild = new Callable("removechild", {
947✔
1234
        signature: {
1235
            args: [new StdlibArgument("child", ValueKind.Dynamic)],
1236
            returns: ValueKind.Boolean,
1237
        },
1238
        impl: (interpreter: Interpreter, child: BrsType) => {
1239
            return BrsBoolean.from(this.removeChildByReference(child));
9✔
1240
        },
1241
    });
1242
    /* If the subject node has been added to a parent node list of children,
1243
    return the parent node, otherwise return invalid.*/
1244
    private getparent = new Callable("getparent", {
947✔
1245
        signature: {
1246
            args: [],
1247
            returns: ValueKind.Dynamic,
1248
        },
1249
        impl: (interpreter: Interpreter) => {
1250
            return this.parent;
8✔
1251
        },
1252
    });
1253

1254
    /* Creates a child node of type nodeType, and adds the new node to the end of the
1255
    subject node list of children */
1256
    private createchild = new Callable("createchild", {
947✔
1257
        signature: {
1258
            args: [new StdlibArgument("nodetype", ValueKind.String)],
1259
            returns: ValueKind.Object,
1260
        },
1261
        impl: (interpreter: Interpreter, nodetype: BrsString) => {
1262
            // currently we can't create a custom subclass object of roSGNode,
1263
            // so we'll always create generic RoSGNode object as child
1264
            let child = createNodeByType(interpreter, nodetype);
17✔
1265
            if (child instanceof RoSGNode) {
17✔
1266
                this.children.push(child);
17✔
1267
                child.setParent(this);
17✔
1268
            }
1269
            return child;
17✔
1270
        },
1271
    });
1272

1273
    /**
1274
     * If the subject node has a child node in the index position, replace that child
1275
     * node with the newChild node in the subject node list of children, otherwise do nothing.
1276
     */
1277

1278
    private replacechild = new Callable("replacechild", {
947✔
1279
        signature: {
1280
            args: [
1281
                new StdlibArgument("newchild", ValueKind.Dynamic),
1282
                new StdlibArgument("index", ValueKind.Int32),
1283
            ],
1284
            returns: ValueKind.Boolean,
1285
        },
1286
        impl: (interpreter: Interpreter, newchild: BrsType, index: Int32) => {
1287
            return BrsBoolean.from(this.replaceChildAtIndex(newchild, index));
9✔
1288
        },
1289
    });
1290

1291
    /**
1292
     * Removes the child nodes specified by child_nodes from the subject node. Returns
1293
     * true if the child nodes were successfully removed.
1294
     */
1295
    private removechildren = new Callable("removechildren", {
947✔
1296
        signature: {
1297
            args: [new StdlibArgument("child_nodes", ValueKind.Dynamic)],
1298
            returns: ValueKind.Boolean,
1299
        },
1300
        impl: (interpreter: Interpreter, child_nodes: BrsType) => {
1301
            if (child_nodes instanceof RoArray) {
6✔
1302
                let childNodesElements = child_nodes.getElements();
6✔
1303
                if (childNodesElements.length !== 0) {
6✔
1304
                    childNodesElements.forEach((childNode) => {
5✔
1305
                        this.removeChildByReference(childNode);
11✔
1306
                    });
1307
                    return BrsBoolean.True;
5✔
1308
                }
1309
            }
1310
            return BrsBoolean.False;
1✔
1311
        },
1312
    });
1313

1314
    /**
1315
     * Removes the number of child nodes specified by num_children from the subject node
1316
     * starting at the position specified by index.
1317
     */
1318
    private removechildrenindex = new Callable("removechildrenindex", {
947✔
1319
        signature: {
1320
            args: [
1321
                new StdlibArgument("num_children", ValueKind.Int32),
1322
                new StdlibArgument("index", ValueKind.Int32),
1323
            ],
1324
            returns: ValueKind.Boolean,
1325
        },
1326
        impl: (interpreter: Interpreter, num_children: Int32, index: Int32) => {
1327
            let numChildrenValue = num_children.getValue();
5✔
1328
            let indexValue = index.getValue();
5✔
1329

1330
            if (numChildrenValue > 0) {
5✔
1331
                let removedChildren = this.children.splice(indexValue, numChildrenValue);
4✔
1332
                removedChildren.forEach((node) => {
4✔
1333
                    node.removeParent();
5✔
1334
                });
1335
                return BrsBoolean.True;
4✔
1336
            }
1337
            return BrsBoolean.False;
1✔
1338
        },
1339
    });
1340

1341
    /**
1342
     * If the subject node has a child node at the index position, return it, otherwise
1343
     * return invalid.
1344
     */
1345
    private getchild = new Callable("getchild", {
947✔
1346
        signature: {
1347
            args: [new StdlibArgument("index", ValueKind.Int32)],
1348
            returns: ValueKind.Dynamic,
1349
        },
1350
        impl: (interpreter: Interpreter, index: Int32) => {
1351
            let indexValue = index.getValue();
5✔
1352
            let childrenSize = this.children.length;
5✔
1353

1354
            if (indexValue >= 0 && indexValue < childrenSize) {
5✔
1355
                return this.children[indexValue];
3✔
1356
            }
1357
            return BrsInvalid.Instance;
2✔
1358
        },
1359
    });
1360

1361
    /**
1362
     * Appends the nodes specified by child_nodes to the subject node.
1363
     */
1364
    private appendchildren = new Callable("appendchildren", {
947✔
1365
        signature: {
1366
            args: [new StdlibArgument("child_nodes", ValueKind.Dynamic)],
1367
            returns: ValueKind.Boolean,
1368
        },
1369
        impl: (interpreter: Interpreter, child_nodes: BrsType) => {
1370
            if (child_nodes instanceof RoArray) {
5✔
1371
                let childNodesElements = child_nodes.getElements();
5✔
1372
                if (childNodesElements.length !== 0) {
5✔
1373
                    childNodesElements.forEach((childNode) => {
4✔
1374
                        if (childNode instanceof RoSGNode) {
8✔
1375
                            // Remove if it exists to reappend
1376
                            this.removeChildByReference(childNode);
8✔
1377
                            this.appendChildToParent(childNode);
8✔
1378
                        }
1379
                    });
1380
                    return BrsBoolean.True;
4✔
1381
                }
1382
            }
1383
            return BrsBoolean.False;
1✔
1384
        },
1385
    });
1386

1387
    /** Creates the number of children specified by num_children for the subject node,
1388
     *  of the type or extended type specified by subtype.
1389
     */
1390
    private createchildren = new Callable("createchildren", {
947✔
1391
        signature: {
1392
            args: [
1393
                new StdlibArgument("num_children", ValueKind.Int32),
1394
                new StdlibArgument("subtype", ValueKind.String),
1395
            ],
1396
            returns: ValueKind.Dynamic,
1397
        },
1398
        impl: (interpreter: Interpreter, num_children: Int32, subtype: BrsString) => {
1399
            let numChildrenValue = num_children.getValue();
3✔
1400
            let addedChildren: RoSGNode[] = [];
3✔
1401
            for (let i = 0; i < numChildrenValue; i++) {
3✔
1402
                let child = createNodeByType(interpreter, subtype);
4✔
1403
                if (child instanceof RoSGNode) {
4✔
1404
                    this.children.push(child);
4✔
1405
                    addedChildren.push(child);
4✔
1406
                    child.setParent(this);
4✔
1407
                }
1408
            }
1409
            return new RoArray(addedChildren);
3✔
1410
        },
1411
    });
1412

1413
    /** Replaces the child nodes in the subject node, starting at the position specified
1414
     *  by index, with new child nodes specified by child_nodes.
1415
     */
1416
    private replacechildren = new Callable("replacechildren", {
947✔
1417
        signature: {
1418
            args: [
1419
                new StdlibArgument("child_nodes", ValueKind.Dynamic),
1420
                new StdlibArgument("index", ValueKind.Int32),
1421
            ],
1422
            returns: ValueKind.Boolean,
1423
        },
1424
        impl: (interpreter: Interpreter, child_nodes: BrsType, index: Int32) => {
1425
            if (child_nodes instanceof RoArray) {
8✔
1426
                let indexValue = index.getValue();
8✔
1427
                let childNodesElements = child_nodes.getElements();
8✔
1428
                if (childNodesElements.length !== 0) {
8✔
1429
                    childNodesElements.forEach((childNode) => {
7✔
1430
                        if (!this.replaceChildAtIndex(childNode, new Int32(indexValue))) {
14✔
1431
                            this.removeChildByReference(childNode);
5✔
1432
                        }
1433
                        indexValue += 1;
14✔
1434
                    });
1435
                    return BrsBoolean.True;
7✔
1436
                }
1437
            }
1438
            return BrsBoolean.False;
1✔
1439
        },
1440
    });
1441

1442
    /**
1443
     * Inserts the child nodes specified by child_nodes to the subject node starting
1444
     * at the position specified by index.
1445
     */
1446
    private insertchildren = new Callable("insertchildren", {
947✔
1447
        signature: {
1448
            args: [
1449
                new StdlibArgument("child_nodes", ValueKind.Dynamic),
1450
                new StdlibArgument("index", ValueKind.Int32),
1451
            ],
1452
            returns: ValueKind.Boolean,
1453
        },
1454
        impl: (interpreter: Interpreter, child_nodes: BrsType, index: Int32) => {
1455
            if (child_nodes instanceof RoArray) {
7✔
1456
                let indexValue = index.getValue();
7✔
1457
                let childNodesElements = child_nodes.getElements();
7✔
1458
                if (childNodesElements.length !== 0) {
7✔
1459
                    childNodesElements.forEach((childNode) => {
6✔
1460
                        this.insertChildAtIndex(childNode, new Int32(indexValue));
12✔
1461
                        indexValue += 1;
12✔
1462
                    });
1463
                    return BrsBoolean.True;
6✔
1464
                }
1465
            }
1466
            return BrsBoolean.False;
1✔
1467
        },
1468
    });
1469

1470
    /**
1471
     * Inserts a previously-created child node at the position index in the subject
1472
     * node list of children, so that this is the position that the new child node
1473
     * is traversed during render.
1474
     */
1475
    private insertchild = new Callable("insertchild", {
947✔
1476
        signature: {
1477
            args: [
1478
                new StdlibArgument("child", ValueKind.Dynamic),
1479
                new StdlibArgument("index", ValueKind.Int32),
1480
            ],
1481
            returns: ValueKind.Boolean,
1482
        },
1483
        impl: (interpreter: Interpreter, child: BrsType, index: Int32) => {
1484
            return BrsBoolean.from(this.insertChildAtIndex(child, index));
6✔
1485
        },
1486
    });
1487

1488
    /**
1489
     * If the subject node has a child node in the index position, remove that child
1490
     * node from the subject node list of children.
1491
     */
1492
    private removechildindex = new Callable("removechildindex", {
947✔
1493
        signature: {
1494
            args: [new StdlibArgument("index", ValueKind.Int32)],
1495
            returns: ValueKind.Boolean,
1496
        },
1497
        impl: (interpreter: Interpreter, index: Int32) => {
1498
            let indexValue = index.getValue();
7✔
1499
            let childrenSize = this.children.length;
7✔
1500

1501
            if (indexValue < childrenSize) {
7✔
1502
                if (indexValue >= 0) {
5✔
1503
                    this.removeChildByReference(this.children[indexValue]);
3✔
1504
                }
1505
                return BrsBoolean.True;
5✔
1506
            }
1507
            return BrsBoolean.False;
2✔
1508
        },
1509
    });
1510

1511
    /**
1512
     * Moves the subject node to another node.
1513
     * If adjustTransform is true, the subject node transformation factor fields (translation/rotation/scale)
1514
     * are adjusted so that the node has the same transformation factors relative to the screen as it previously did.
1515
     * If adjustTransform is false, the subject node is simply parented to the new node without adjusting its
1516
     * transformation factor fields, in which case, the reparenting operation could cause the node to jump to a
1517
     * new position on the screen.
1518
     */
1519
    private reparent = new Callable("reparent", {
947✔
1520
        signature: {
1521
            args: [
1522
                new StdlibArgument("newParent", ValueKind.Dynamic),
1523
                new StdlibArgument("adjustTransform", ValueKind.Boolean),
1524
            ],
1525
            returns: ValueKind.Boolean,
1526
        },
1527
        impl: (interpreter: Interpreter, newParent: BrsType, adjustTransform: BrsBoolean) => {
1528
            if (newParent instanceof RoSGNode && newParent !== this) {
4✔
1529
                // TODO: adjustTransform has to be implemented probably by traversing the
1530
                // entire parent tree to get to the top, calculate the absolute transform
1531
                // parameters and then use that to adjust the new transform properties.
1532
                // Until that is implemented, the parameter does nothing.
1533

1534
                // Remove parents child reference
1535
                if (this.parent instanceof RoSGNode) {
3✔
1536
                    this.parent.removeChildByReference(this);
2✔
1537
                }
1538
                newParent.appendChildToParent(this);
3✔
1539
                return BrsBoolean.True;
3✔
1540
            }
1541
            return BrsBoolean.False;
1✔
1542
        },
1543
    });
1544

1545
    /* Returns true if the subject node has the remote control focus, and false otherwise */
1546
    private hasfocus = new Callable("hasfocus", {
947✔
1547
        signature: {
1548
            args: [],
1549
            returns: ValueKind.Boolean,
1550
        },
1551
        impl: (interpreter: Interpreter) => {
1552
            return BrsBoolean.from(interpreter.environment.getFocusedNode() === this);
13✔
1553
        },
1554
    });
1555

1556
    private boundingRect = new Callable("boundingRect", {
947✔
1557
        signature: {
1558
            args: [],
1559
            returns: ValueKind.Dynamic,
1560
        },
1561
        impl: (interpreter: Interpreter) => {
1562
            return toAssociativeArray({ x: 0, y: 0, width: 0, height: 0 });
2✔
1563
        },
1564
    });
1565

1566
    /**
1567
     * Starting with a leaf node, traverses upward through the parents until it reaches
1568
     * a node without a parent (root node).
1569
     * @param {RoSGNode} node The leaf node to create the tree with
1570
     * @returns RoSGNode[] The parent chain starting with root-most parent
1571
     */
1572
    private createPath(node: RoSGNode): RoSGNode[] {
1573
        let path: RoSGNode[] = [node];
34✔
1574

1575
        while (node.parent instanceof RoSGNode) {
34✔
1576
            path.push(node.parent);
60✔
1577
            node = node.parent;
60✔
1578
        }
1579

1580
        return path.reverse();
34✔
1581
    }
1582

1583
    /**
1584
     *  If on is set to true, sets the current remote control focus to the subject node,
1585
     *  also automatically removing it from the node on which it was previously set.
1586
     *  If on is set to false, removes focus from the subject node if it had it.
1587
     *
1588
     *  It also runs through all of the ancestors of the node that was focused prior to this call,
1589
     *  and the newly focused node, and sets the `focusedChild` field of each to reflect the new state.
1590
     */
1591
    private setfocus = new Callable("setfocus", {
947✔
1592
        signature: {
1593
            args: [new StdlibArgument("on", ValueKind.Boolean)],
1594
            returns: ValueKind.Boolean,
1595
        },
1596
        impl: (interpreter: Interpreter, on: BrsBoolean) => {
1597
            let focusedChildString = new BrsString("focusedchild");
26✔
1598
            let currFocusedNode = interpreter.environment.getFocusedNode();
26✔
1599

1600
            if (on.toBoolean()) {
26✔
1601
                interpreter.environment.setFocusedNode(this);
19✔
1602

1603
                // Get the focus chain, with lowest ancestor first.
1604
                let newFocusChain = this.createPath(this);
19✔
1605

1606
                // If there's already a focused node somewhere, we need to remove focus
1607
                // from it and its ancestors.
1608
                if (currFocusedNode instanceof RoSGNode) {
19✔
1609
                    // Get the focus chain, with root-most ancestor first.
1610
                    let currFocusChain = this.createPath(currFocusedNode);
8✔
1611

1612
                    // Find the lowest common ancestor (LCA) between the newly focused node
1613
                    // and the current focused node.
1614
                    let lcaIndex = 0;
8✔
1615
                    while (lcaIndex < newFocusChain.length && lcaIndex < currFocusChain.length) {
8✔
1616
                        if (currFocusChain[lcaIndex] !== newFocusChain[lcaIndex]) break;
14✔
1617
                        lcaIndex++;
7✔
1618
                    }
1619

1620
                    // Unset all of the not-common ancestors of the current focused node.
1621
                    for (let i = lcaIndex; i < currFocusChain.length; i++) {
8✔
1622
                        currFocusChain[i].set(focusedChildString, BrsInvalid.Instance);
15✔
1623
                    }
1624
                }
1625

1626
                // Set the focusedChild for each ancestor to the next node in the chain,
1627
                // which is the current node's child.
1628
                for (let i = 0; i < newFocusChain.length - 1; i++) {
19✔
1629
                    newFocusChain[i].set(focusedChildString, newFocusChain[i + 1]);
31✔
1630
                }
1631

1632
                // Finally, set the focusedChild of the newly focused node to itself (to mimic RBI behavior).
1633
                this.set(focusedChildString, this);
19✔
1634
            } else {
1635
                interpreter.environment.setFocusedNode(BrsInvalid.Instance);
7✔
1636

1637
                // If we're unsetting focus on ourself, we need to unset it on all ancestors as well.
1638
                if (currFocusedNode === this) {
7!
1639
                    // Get the focus chain, with root-most ancestor first.
1640
                    let currFocusChain = this.createPath(currFocusedNode);
7✔
1641
                    currFocusChain.forEach((node) => {
7✔
1642
                        node.set(focusedChildString, BrsInvalid.Instance);
22✔
1643
                    });
1644
                } else {
1645
                    // If the node doesn't have focus already, and it's not gaining focus,
1646
                    // we don't need to notify any ancestors.
1647
                    this.set(focusedChildString, BrsInvalid.Instance);
×
1648
                }
1649
            }
1650

1651
            return BrsBoolean.False; //brightscript always returns false for some reason
26✔
1652
        },
1653
    });
1654

1655
    /**
1656
     *  Returns true if the subject node or any of its descendants in the SceneGraph node tree
1657
     *  has remote control focus
1658
     */
1659
    private isinfocuschain = new Callable("isinfocuschain", {
947✔
1660
        signature: {
1661
            args: [],
1662
            returns: ValueKind.Boolean,
1663
        },
1664
        impl: (interpreter: Interpreter) => {
1665
            // loop through all children DFS and check if any children has focus
1666
            if (interpreter.environment.getFocusedNode() === this) {
58✔
1667
                return BrsBoolean.True;
10✔
1668
            }
1669

1670
            return BrsBoolean.from(this.isChildrenFocused(interpreter));
48✔
1671
        },
1672
    });
1673

1674
    /* Returns the node that is a descendant of the nearest component ancestor of the subject node whose id field matches the given name,
1675
        otherwise return invalid.
1676
        Implemented as a DFS from the top of parent hierarchy to match the observed behavior as opposed to the BFS mentioned in the docs. */
1677
    private findnode = new Callable("findnode", {
947✔
1678
        signature: {
1679
            args: [new StdlibArgument("name", ValueKind.String)],
1680
            returns: ValueKind.Dynamic,
1681
        },
1682
        impl: (interpreter: Interpreter, name: BrsString) => {
1683
            // Roku's implementation returns invalid on empty string
1684
            if (name.value.length === 0) return BrsInvalid.Instance;
46✔
1685

1686
            // climb parent hierarchy to find node to start search at
1687
            let root: RoSGNode = this;
44✔
1688
            while (root.parent && root.parent instanceof RoSGNode) {
44✔
1689
                root = root.parent;
10✔
1690
            }
1691

1692
            // perform search
1693
            return this.findNodeById(root, name);
44✔
1694
        },
1695
    });
1696

1697
    /* Checks whether the subtype of the subject node is a descendant of the subtype nodeType
1698
     * in the SceneGraph node class hierarchy.
1699
     *
1700
     *
1701
     */
1702
    private issubtype = new Callable("issubtype", {
947✔
1703
        signature: {
1704
            args: [new StdlibArgument("nodeType", ValueKind.String)],
1705
            returns: ValueKind.Boolean,
1706
        },
1707
        impl: (interpreter: Interpreter, nodeType: BrsString) => {
1708
            return BrsBoolean.from(isSubtypeCheck(this.nodeSubtype, nodeType.value));
15✔
1709
        },
1710
    });
1711

1712
    /* Checks whether the subtype of the subject node is a descendant of the subtype nodeType
1713
     * in the SceneGraph node class hierarchy.
1714
     */
1715
    private parentsubtype = new Callable("parentsubtype", {
947✔
1716
        signature: {
1717
            args: [new StdlibArgument("nodeType", ValueKind.String)],
1718
            returns: ValueKind.Object,
1719
        },
1720
        impl: (interpreter: Interpreter, nodeType: BrsString) => {
1721
            const parentType = subtypeHierarchy.get(nodeType.value.toLowerCase());
4✔
1722
            if (parentType) {
4✔
1723
                return new BrsString(parentType);
3✔
1724
            }
1725
            return BrsInvalid.Instance;
1✔
1726
        },
1727
    });
1728

1729
    /* Returns a Boolean value indicating whether the roSGNode parameter
1730
            refers to the same node object as this node */
1731
    private issamenode = new Callable("issamenode", {
947✔
1732
        signature: {
1733
            args: [new StdlibArgument("roSGNode", ValueKind.Dynamic)],
1734
            returns: ValueKind.Boolean,
1735
        },
1736
        impl: (interpreter: Interpreter, roSGNode: RoSGNode) => {
1737
            return BrsBoolean.from(this === roSGNode);
4✔
1738
        },
1739
    });
1740

1741
    /* Returns the subtype of this node as specified when it was created */
1742
    private subtype = new Callable("subtype", {
947✔
1743
        signature: {
1744
            args: [],
1745
            returns: ValueKind.String,
1746
        },
1747
        impl: (interpreter: Interpreter) => {
1748
            return new BrsString(this.nodeSubtype);
38✔
1749
        },
1750
    });
1751

1752
    /* Takes a list of models and creates fields with default values, and adds them to this.fields. */
1753
    protected registerDefaultFields(fields: FieldModel[]) {
1754
        fields.forEach((field) => {
1,221✔
1755
            let fieldType: FieldKind | undefined;
1756
            let value: BrsType | undefined;
1757
            if (field.name === "font") {
10,430✔
1758
                // TODO: Special case for Scene, should be handled in a more generic way
1759
                value = NodeFactory.createNode(BrsNodeType.Font) ?? BrsInvalid.Instance;
14!
1760
                fieldType = FieldKind.Node;
14✔
1761
            } else {
1762
                value = getBrsValueFromFieldType(field.type, field.value);
10,416✔
1763
                fieldType = FieldKind.fromString(field.type);
10,416✔
1764
            }
1765
            if (fieldType) {
10,430✔
1766
                this.fields.set(
10,430✔
1767
                    field.name.toLowerCase(),
1768
                    new Field(value, fieldType, !!field.alwaysNotify, field.hidden)
1769
                );
1770
            }
1771
        });
1772
    }
1773

1774
    /**
1775
     * Takes a list of preset fields and creates fields from them.
1776
     * TODO: filter out any non-allowed members. For example, if we instantiate a Node like this:
1777
     *      <Node thisisnotanodefield="fakevalue" />
1778
     * then Roku logs an error, because Node does not have a property called "thisisnotanodefield".
1779
     */
1780
    protected registerInitializedFields(fields: AAMember[]) {
1781
        fields.forEach((field) => {
1,191✔
1782
            let fieldType = FieldKind.fromBrsType(field.value);
503✔
1783
            if (fieldType) {
503✔
1784
                this.fields.set(
502✔
1785
                    field.name.value.toLowerCase(),
1786
                    new Field(field.value, fieldType, false)
1787
                );
1788
            }
1789
        });
1790
    }
1791
}
1792

1793
// A node that represents the m.global, referenced by all other nodes
1794
export const mGlobal = new RoSGNode([]);
143✔
1795

1796
export function createNodeByType(interpreter: Interpreter, type: BrsString): RoSGNode | BrsInvalid {
143✔
1797
    // If the requested component has been mocked, construct and return the mock instead
1798
    let maybeMock = interpreter.environment.getMockObject(type.value.toLowerCase());
232✔
1799
    if (maybeMock instanceof RoAssociativeArray) {
232✔
1800
        let mock: typeof MockNodeModule = require("../../extensions/MockNode");
1✔
1801
        return new mock.MockNode(maybeMock, type.value);
1✔
1802
    }
1803

1804
    // If this is a built-in node component, then return it.
1805
    let node = NodeFactory.createNode(type.value as BrsNodeType);
231✔
1806
    if (node) {
231✔
1807
        return node;
157✔
1808
    }
1809

1810
    let typeDef = interpreter.environment.nodeDefMap.get(type.value.toLowerCase());
74✔
1811
    if (typeDef) {
74✔
1812
        //use typeDef object to tack on all the bells & whistles of a custom node
1813
        let typeDefStack: ComponentDefinition[] = [];
72✔
1814
        let currentEnv = typeDef.environment?.createSubEnvironment();
72!
1815

1816
        // Adding all component extensions to the stack to call init methods
1817
        // in the correct order.
1818
        typeDefStack.push(typeDef);
72✔
1819
        while (typeDef) {
72✔
1820
            // Add the current typedef to the subtypeHierarchy
1821
            subtypeHierarchy.set(typeDef.name!.toLowerCase(), typeDef.extends || "Node");
81✔
1822

1823
            typeDef = interpreter.environment.nodeDefMap.get(typeDef.extends?.toLowerCase());
81✔
1824
            if (typeDef) typeDefStack.push(typeDef);
81✔
1825
        }
1826

1827
        // Start from the "basemost" component of the tree.
1828
        typeDef = typeDefStack.pop();
72✔
1829

1830
        // If this extends a built-in node component, create it.
1831
        let node = NodeFactory.createNode(typeDef!.extends as BrsNodeType, type.value);
72✔
1832

1833
        // Default to Node as parent.
1834
        if (!node) {
72✔
1835
            node = new RoSGNode([], type.value);
68✔
1836
        }
1837
        let mPointer = new RoAssociativeArray([]);
72✔
1838
        currentEnv?.setM(new RoAssociativeArray([]));
72!
1839

1840
        // Add children, fields and call each init method starting from the
1841
        // "basemost" component of the tree.
1842
        while (typeDef) {
72✔
1843
            let init: BrsType;
1844

1845
            interpreter.inSubEnv((subInterpreter) => {
81✔
1846
                addChildren(subInterpreter, node!, typeDef!);
81✔
1847
                addFields(subInterpreter, node!, typeDef!);
81✔
1848
                return BrsInvalid.Instance;
81✔
1849
            }, currentEnv);
1850

1851
            interpreter.inSubEnv((subInterpreter) => {
81✔
1852
                init = subInterpreter.getInitMethod();
81✔
1853
                return BrsInvalid.Instance;
81✔
1854
            }, typeDef.environment);
1855

1856
            interpreter.inSubEnv((subInterpreter) => {
81✔
1857
                subInterpreter.environment.hostNode = node;
81✔
1858

1859
                mPointer.set(new BrsString("top"), node!);
81✔
1860
                mPointer.set(new BrsString("global"), mGlobal);
81✔
1861
                subInterpreter.environment.setM(mPointer);
81✔
1862
                subInterpreter.environment.setRootM(mPointer);
81✔
1863
                node!.m = mPointer;
81✔
1864
                if (init instanceof Callable) {
81✔
1865
                    init.call(subInterpreter);
35✔
1866
                }
1867
                return BrsInvalid.Instance;
81✔
1868
            }, currentEnv);
1869

1870
            typeDef = typeDefStack.pop();
81✔
1871
        }
1872

1873
        return node;
72✔
1874
    } else {
1875
        interpreter.stderr.write(
2✔
1876
            `BRIGHTSCRIPT: ERROR: roSGNode: Failed to create roSGNode with type ${
1877
                type.value
1878
            }: ${interpreter.formatLocation()}\n`
1879
        );
1880
        return BrsInvalid.Instance;
2✔
1881
    }
1882
}
1883

1884
function addFields(interpreter: Interpreter, node: RoSGNode, typeDef: ComponentDefinition) {
1885
    let fields = typeDef.fields;
81✔
1886
    for (let [key, value] of Object.entries(fields)) {
81✔
1887
        if (value instanceof Object) {
53✔
1888
            // Roku throws a run-time error if any fields are duplicated between inherited components.
1889
            // TODO: throw exception when fields are duplicated.
1890
            let fieldName = new BrsString(key);
53✔
1891

1892
            let addField = node.getMethod("addField");
53✔
1893
            if (addField) {
53✔
1894
                addField.call(
53✔
1895
                    interpreter,
1896
                    fieldName,
1897
                    new BrsString(value.type),
1898
                    BrsBoolean.from(value.alwaysNotify === "true")
1899
                );
1900
            }
1901

1902
            // set default value if it was specified in xml
1903
            let setField = node.getMethod("setField");
53✔
1904
            if (setField && value.value) {
53✔
1905
                setField.call(
31✔
1906
                    interpreter,
1907
                    fieldName,
1908
                    getBrsValueFromFieldType(value.type, value.value)
1909
                );
1910
            }
1911

1912
            // Add the onChange callback if it exists.
1913
            if (value.onChange) {
53✔
1914
                let field = node.getFields().get(fieldName.value.toLowerCase());
14✔
1915
                let callableFunction = interpreter.getCallableFunction(value.onChange);
14✔
1916
                if (callableFunction && field) {
14✔
1917
                    // observers set via `onChange` can never be removed, despite RBI's documentation claiming
1918
                    // that "[i]t is equivalent to calling the ifSGNodeField observeField() method".
1919
                    field.addObserver(
13✔
1920
                        "permanent",
1921
                        interpreter,
1922
                        callableFunction,
1923
                        node,
1924
                        node,
1925
                        fieldName
1926
                    );
1927
                }
1928
            }
1929
        }
1930
    }
1931
}
1932

1933
function addChildren(
1934
    interpreter: Interpreter,
1935
    node: RoSGNode,
1936
    typeDef: ComponentDefinition | ComponentNode
1937
) {
1938
    let children = typeDef.children;
98✔
1939
    let appendChild = node.getMethod("appendchild");
98✔
1940

1941
    children.forEach((child) => {
98✔
1942
        let newChild = createNodeByType(interpreter, new BrsString(child.name));
122✔
1943
        if (newChild instanceof RoSGNode) {
122✔
1944
            if (appendChild) {
120✔
1945
                appendChild.call(interpreter, newChild);
120✔
1946
                let setField = newChild.getMethod("setfield");
120✔
1947
                if (setField) {
120✔
1948
                    let nodeFields = newChild.getFields();
120✔
1949
                    for (let [key, value] of Object.entries(child.fields)) {
120✔
1950
                        let field = nodeFields.get(key.toLowerCase());
304✔
1951
                        if (field) {
304✔
1952
                            setField.call(
304✔
1953
                                interpreter,
1954
                                new BrsString(key),
1955
                                // use the field type to construct the field value
1956
                                getBrsValueFromFieldType(field.getType(), value)
1957
                            );
1958
                        }
1959
                    }
1960
                }
1961
            }
1962

1963
            if (child.children.length > 0) {
120✔
1964
                // we need to add the child's own children
1965
                addChildren(interpreter, newChild, child);
17✔
1966
            }
1967
        }
1968
    });
1969
}
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